Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h
index 88863869d..52ff30edf 100644
--- a/src/interfaces/wallet.h
+++ b/src/interfaces/wallet.h
@@ -1,413 +1,413 @@
// Copyright (c) 2018 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_INTERFACES_WALLET_H
#define BITCOIN_INTERFACES_WALLET_H
#include <consensus/amount.h>
#include <interfaces/chain.h> // For ChainClient
#include <primitives/blockhash.h>
#include <primitives/transaction.h> // For CTxOut
#include <pubkey.h> // For CKeyID and CScriptID (definitions needed in CTxDestination instantiation)
#include <script/sighashtype.h>
#include <script/standard.h> // For CTxDestination
#include <support/allocators/secure.h> // For SecureString
#include <util/message.h>
+#include <util/result.h>
#include <util/ui_change_type.h>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
class CChainParams;
class CCoinControl;
class CKey;
class CMutableTransaction;
class COutPoint;
class CTransaction;
class CWallet;
enum class FeeReason;
enum class OutputType;
enum class TransactionError;
enum isminetype : unsigned int;
struct CRecipient;
struct PartiallySignedTransaction;
struct WalletContext;
typedef uint8_t isminefilter;
struct TxId;
struct bilingual_str;
namespace interfaces {
class Handler;
struct WalletAddress;
struct WalletBalances;
struct WalletTx;
struct WalletTxOut;
struct WalletTxStatus;
using WalletOrderForm = std::vector<std::pair<std::string, std::string>>;
using WalletValueMap = std::map<std::string, std::string>;
//! Interface for accessing a wallet.
class Wallet {
public:
virtual ~Wallet() {}
//! Encrypt wallet.
virtual bool encryptWallet(const SecureString &wallet_passphrase) = 0;
//! Return whether wallet is encrypted.
virtual bool isCrypted() = 0;
//! Lock wallet.
virtual bool lock() = 0;
//! Unlock wallet.
virtual bool unlock(const SecureString &wallet_passphrase) = 0;
//! Return whether wallet is locked.
virtual bool isLocked() = 0;
//! Change wallet passphrase.
virtual bool
changeWalletPassphrase(const SecureString &old_wallet_passphrase,
const SecureString &new_wallet_passphrase) = 0;
//! Abort a rescan.
virtual void abortRescan() = 0;
//! Back up wallet.
virtual bool backupWallet(const std::string &filename) = 0;
//! Get wallet name.
virtual std::string getWalletName() = 0;
//! Get chainparams.
virtual const CChainParams &getChainParams() = 0;
// Get a new address.
virtual bool getNewDestination(const OutputType type,
const std::string label,
CTxDestination &dest) = 0;
//! Get public key.
virtual bool getPubKey(const CScript &script, const CKeyID &address,
CPubKey &pub_key) = 0;
//! Sign message
virtual SigningResult signMessage(const std::string &message,
const PKHash &pkhash,
std::string &str_sig) = 0;
//! Return whether wallet has private key.
virtual bool isSpendable(const CTxDestination &dest) = 0;
//! Return whether wallet has watch only keys.
virtual bool haveWatchOnly() = 0;
//! Add or update address.
virtual bool setAddressBook(const CTxDestination &dest,
const std::string &name,
const std::string &purpose) = 0;
// Remove address.
virtual bool delAddressBook(const CTxDestination &dest) = 0;
//! Look up address in wallet, return whether exists.
virtual bool getAddress(const CTxDestination &dest, std::string *name,
isminetype *is_mine, std::string *purpose) = 0;
//! Get wallet address list.
virtual std::vector<WalletAddress> getAddresses() = 0;
//! Add dest data.
virtual bool addDestData(const CTxDestination &dest, const std::string &key,
const std::string &value) = 0;
//! Erase dest data.
virtual bool eraseDestData(const CTxDestination &dest,
const std::string &key) = 0;
//! Get dest values with prefix.
virtual std::vector<std::string>
getDestValues(const std::string &prefix) = 0;
//! Lock coin.
virtual void lockCoin(const COutPoint &output) = 0;
//! Unlock coin.
virtual void unlockCoin(const COutPoint &output) = 0;
//! Return whether coin is locked.
virtual bool isLockedCoin(const COutPoint &output) = 0;
//! List locked coins.
virtual void listLockedCoins(std::vector<COutPoint> &outputs) = 0;
//! Create transaction.
- virtual CTransactionRef
+ virtual util::Result<CTransactionRef>
createTransaction(const std::vector<CRecipient> &recipients,
const CCoinControl &coin_control, bool sign,
- int &change_pos, Amount &fee,
- bilingual_str &fail_reason) = 0;
+ int &change_pos, Amount &fee) = 0;
//! Commit transaction.
virtual void commitTransaction(CTransactionRef tx, WalletValueMap value_map,
WalletOrderForm order_form) = 0;
//! Return whether transaction can be abandoned.
virtual bool transactionCanBeAbandoned(const TxId &txid) = 0;
//! Abandon transaction.
virtual bool abandonTransaction(const TxId &txid) = 0;
//! Get a transaction.
virtual CTransactionRef getTx(const TxId &txid) = 0;
//! Get transaction information.
virtual WalletTx getWalletTx(const TxId &txid) = 0;
//! Get list of all wallet transactions.
virtual std::vector<WalletTx> getWalletTxs() = 0;
//! Try to get updated status for a particular transaction, if possible
//! without blocking.
virtual bool tryGetTxStatus(const TxId &txid, WalletTxStatus &tx_status,
int &num_blocks, int64_t &block_time) = 0;
//! Get transaction details.
virtual WalletTx getWalletTxDetails(const TxId &txid,
WalletTxStatus &tx_status,
WalletOrderForm &order_form,
bool &in_mempool, int &num_blocks) = 0;
//! Fill PSBT.
virtual TransactionError fillPSBT(SigHashType sighash_type, bool sign,
bool bip32derivs,
PartiallySignedTransaction &psbtx,
bool &complete) const = 0;
//! Get balances.
virtual WalletBalances getBalances() = 0;
//! Get balances if possible without blocking.
virtual bool tryGetBalances(WalletBalances &balances,
BlockHash &block_hash) = 0;
//! Get balance.
virtual Amount getBalance() = 0;
//! Get available balance.
virtual Amount getAvailableBalance(const CCoinControl &coin_control) = 0;
//! Return whether transaction input belongs to wallet.
virtual isminetype txinIsMine(const CTxIn &txin) = 0;
//! Return whether transaction output belongs to wallet.
virtual isminetype txoutIsMine(const CTxOut &txout) = 0;
//! Return debit amount if transaction input belongs to wallet.
virtual Amount getDebit(const CTxIn &txin, isminefilter filter) = 0;
//! Return credit amount if transaction input belongs to wallet.
virtual Amount getCredit(const CTxOut &txout, isminefilter filter) = 0;
//! Return AvailableCoins + LockedCoins grouped by wallet address.
//! (put change in one group with wallet address)
using CoinsList = std::map<CTxDestination,
std::vector<std::tuple<COutPoint, WalletTxOut>>>;
virtual CoinsList listCoins() = 0;
//! Return wallet transaction output information.
virtual std::vector<WalletTxOut>
getCoins(const std::vector<COutPoint> &outputs) = 0;
//! Get required fee.
virtual Amount getRequiredFee(unsigned int tx_bytes) = 0;
//! Get minimum fee.
virtual Amount getMinimumFee(unsigned int tx_bytes,
const CCoinControl &coin_control) = 0;
// Return whether HD enabled.
virtual bool hdEnabled() = 0;
// Return whether the wallet is blank.
virtual bool canGetAddresses() const = 0;
// Return whether private keys enabled.
virtual bool privateKeysDisabled() = 0;
// Get default address type.
virtual OutputType getDefaultAddressType() = 0;
//! Get max tx fee.
virtual Amount getDefaultMaxTxFee() = 0;
// Remove wallet.
virtual void remove() = 0;
//! Return whether is a legacy wallet
virtual bool isLegacy() = 0;
//! Register handler for unload message.
using UnloadFn = std::function<void()>;
virtual std::unique_ptr<Handler> handleUnload(UnloadFn fn) = 0;
//! Register handler for show progress messages.
using ShowProgressFn =
std::function<void(const std::string &title, int progress)>;
virtual std::unique_ptr<Handler> handleShowProgress(ShowProgressFn fn) = 0;
//! Register handler for status changed messages.
using StatusChangedFn = std::function<void()>;
virtual std::unique_ptr<Handler>
handleStatusChanged(StatusChangedFn fn) = 0;
//! Register handler for address book changed messages.
using AddressBookChangedFn = std::function<void(
const CTxDestination &address, const std::string &label, bool is_mine,
const std::string &purpose, ChangeType status)>;
virtual std::unique_ptr<Handler>
handleAddressBookChanged(AddressBookChangedFn fn) = 0;
//! Register handler for transaction changed messages.
using TransactionChangedFn =
std::function<void(const TxId &txid, ChangeType status)>;
virtual std::unique_ptr<Handler>
handleTransactionChanged(TransactionChangedFn fn) = 0;
//! Register handler for watchonly changed messages.
using WatchOnlyChangedFn = std::function<void(bool have_watch_only)>;
virtual std::unique_ptr<Handler>
handleWatchOnlyChanged(WatchOnlyChangedFn fn) = 0;
//! Register handler for keypool changed messages.
using CanGetAddressesChangedFn = std::function<void()>;
virtual std::unique_ptr<Handler>
handleCanGetAddressesChanged(CanGetAddressesChangedFn fn) = 0;
//! Return pointer to internal wallet class, useful for testing.
virtual CWallet *wallet() { return nullptr; }
};
//! Wallet chain client that in addition to having chain client methods for
//! starting up, shutting down, and registering RPCs, also has additional
//! methods (called by the GUI) to load and create wallets.
class WalletClient : public ChainClient {
public:
//! Create new wallet.
virtual std::unique_ptr<Wallet>
createWallet(const std::string &name, const SecureString &passphrase,
uint64_t wallet_creation_flags, bilingual_str &error,
std::vector<bilingual_str> &warnings) = 0;
//! Load existing wallet.
virtual std::unique_ptr<Wallet>
loadWallet(const std::string &name, bilingual_str &error,
std::vector<bilingual_str> &warnings) = 0;
//! Return default wallet directory.
virtual std::string getWalletDir() = 0;
//! Return available wallets in wallet directory.
virtual std::vector<std::string> listWalletDir() = 0;
//! Return interfaces for accessing wallets (if any).
virtual std::vector<std::unique_ptr<Wallet>> getWallets() = 0;
//! Register handler for load wallet messages. This callback is triggered by
//! createWallet and loadWallet above, and also triggered when wallets are
//! loaded at startup or by RPC.
using LoadWalletFn = std::function<void(std::unique_ptr<Wallet> wallet)>;
virtual std::unique_ptr<Handler> handleLoadWallet(LoadWalletFn fn) = 0;
};
//! Information about one wallet address.
struct WalletAddress {
CTxDestination dest;
isminetype is_mine;
std::string name;
std::string purpose;
WalletAddress(CTxDestination destIn, isminetype isMineIn,
std::string nameIn, std::string purposeIn)
: dest(std::move(destIn)), is_mine(isMineIn), name(std::move(nameIn)),
purpose(std::move(purposeIn)) {}
};
//! Collection of wallet balances.
struct WalletBalances {
Amount balance = Amount::zero();
Amount unconfirmed_balance = Amount::zero();
Amount immature_balance = Amount::zero();
bool have_watch_only = false;
Amount watch_only_balance = Amount::zero();
Amount unconfirmed_watch_only_balance = Amount::zero();
Amount immature_watch_only_balance = Amount::zero();
bool balanceChanged(const WalletBalances &prev) const {
return balance != prev.balance ||
unconfirmed_balance != prev.unconfirmed_balance ||
immature_balance != prev.immature_balance ||
watch_only_balance != prev.watch_only_balance ||
unconfirmed_watch_only_balance !=
prev.unconfirmed_watch_only_balance ||
immature_watch_only_balance != prev.immature_watch_only_balance;
}
};
// Wallet transaction information.
struct WalletTx {
CTransactionRef tx;
std::vector<isminetype> txin_is_mine;
std::vector<isminetype> txout_is_mine;
std::vector<CTxDestination> txout_address;
std::vector<isminetype> txout_address_is_mine;
Amount credit;
Amount debit;
Amount change;
int64_t time;
std::map<std::string, std::string> value_map;
bool is_coinbase;
};
//! Updated transaction status.
struct WalletTxStatus {
int block_height;
int blocks_to_maturity;
int depth_in_main_chain;
unsigned int time_received;
uint32_t lock_time;
bool is_trusted;
bool is_abandoned;
bool is_coinbase;
bool is_in_main_chain;
};
//! Wallet transaction output.
struct WalletTxOut {
CTxOut txout;
int64_t time;
int depth_in_main_chain = -1;
bool is_spent = false;
};
//! Return implementation of Wallet interface. This function is defined in
//! dummywallet.cpp and throws if the wallet component is not compiled.
std::unique_ptr<Wallet> MakeWallet(const std::shared_ptr<CWallet> &wallet);
//! Return implementation of ChainClient interface for a wallet client. This
//! function will be undefined in builds where ENABLE_WALLET is false.
std::unique_ptr<WalletClient> MakeWalletClient(Chain &chain, ArgsManager &args);
} // namespace interfaces
#endif // BITCOIN_INTERFACES_WALLET_H
diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp
index 5eec04bec..f94dd270d 100644
--- a/src/qt/walletmodel.cpp
+++ b/src/qt/walletmodel.cpp
@@ -1,534 +1,536 @@
// Copyright (c) 2011-2019 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <qt/walletmodel.h>
#include <cashaddrenc.h>
#include <common/args.h> // for GetBoolArg
#include <interfaces/handler.h>
#include <interfaces/node.h>
#include <key_io.h>
#include <node/ui_interface.h>
#include <psbt.h>
#include <qt/addresstablemodel.h>
#include <qt/clientmodel.h>
#include <qt/guiconstants.h>
#include <qt/paymentserver.h>
#include <qt/recentrequeststablemodel.h>
#include <qt/transactiontablemodel.h>
#include <util/translation.h>
#include <wallet/coincontrol.h>
#include <wallet/wallet.h> // for CRecipient
#include <QDebug>
#include <QSet>
#include <QTimer>
#include <cstdint>
#include <functional>
WalletModel::WalletModel(std::unique_ptr<interfaces::Wallet> wallet,
ClientModel &client_model,
const PlatformStyle *platformStyle, QObject *parent)
: QObject(parent), m_wallet(std::move(wallet)),
m_client_model(&client_model), m_node(client_model.node()),
optionsModel(client_model.getOptionsModel()), addressTableModel(nullptr),
transactionTableModel(nullptr), recentRequestsTableModel(nullptr),
cachedEncryptionStatus(Unencrypted), timer(new QTimer(this)) {
fHaveWatchOnly = m_wallet->haveWatchOnly();
addressTableModel = new AddressTableModel(this);
transactionTableModel = new TransactionTableModel(platformStyle, this);
recentRequestsTableModel = new RecentRequestsTableModel(this);
subscribeToCoreSignals();
}
WalletModel::~WalletModel() {
unsubscribeFromCoreSignals();
}
void WalletModel::startPollBalance() {
// This timer will be fired repeatedly to update the balance
connect(timer, &QTimer::timeout, this, &WalletModel::pollBalanceChanged);
timer->start(MODEL_UPDATE_DELAY);
}
void WalletModel::setClientModel(ClientModel *client_model) {
m_client_model = client_model;
if (!m_client_model) {
timer->stop();
}
}
void WalletModel::updateStatus() {
EncryptionStatus newEncryptionStatus = getEncryptionStatus();
if (cachedEncryptionStatus != newEncryptionStatus) {
Q_EMIT encryptionStatusChanged();
}
}
void WalletModel::pollBalanceChanged() {
// Avoid recomputing wallet balances unless a TransactionChanged or
// BlockTip notification was received.
if (!fForceCheckBalanceChanged &&
m_cached_last_update_tip == getLastBlockProcessed()) {
return;
}
// Try to get balances and return early if locks can't be acquired. This
// avoids the GUI from getting stuck on periodical polls if the core is
// holding the locks for a longer time - for example, during a wallet
// rescan.
interfaces::WalletBalances new_balances;
BlockHash block_hash;
if (!m_wallet->tryGetBalances(new_balances, block_hash)) {
return;
}
if (fForceCheckBalanceChanged || block_hash != m_cached_last_update_tip) {
fForceCheckBalanceChanged = false;
// Balance and number of transactions might have changed
m_cached_last_update_tip = block_hash;
checkBalanceChanged(new_balances);
if (transactionTableModel) {
transactionTableModel->updateConfirmations();
}
}
}
void WalletModel::checkBalanceChanged(
const interfaces::WalletBalances &new_balances) {
if (new_balances.balanceChanged(m_cached_balances)) {
m_cached_balances = new_balances;
Q_EMIT balanceChanged(new_balances);
}
}
void WalletModel::updateTransaction() {
// Balance and number of transactions might have changed
fForceCheckBalanceChanged = true;
}
void WalletModel::updateAddressBook(const QString &address,
const QString &label, bool isMine,
const QString &purpose, int status) {
if (addressTableModel) {
addressTableModel->updateEntry(address, label, isMine, purpose, status);
}
}
void WalletModel::updateWatchOnlyFlag(bool fHaveWatchonly) {
fHaveWatchOnly = fHaveWatchonly;
Q_EMIT notifyWatchonlyChanged(fHaveWatchonly);
}
bool WalletModel::validateAddress(const QString &address) {
return IsValidDestinationString(address.toStdString(), getChainParams());
}
WalletModel::SendCoinsReturn
WalletModel::prepareTransaction(WalletModelTransaction &transaction,
const CCoinControl &coinControl) {
Amount total = Amount::zero();
bool fSubtractFeeFromAmount = false;
QList<SendCoinsRecipient> recipients = transaction.getRecipients();
std::vector<CRecipient> vecSend;
if (recipients.empty()) {
return OK;
}
// Used to detect duplicates
QSet<QString> setAddress;
int nAddresses = 0;
// Pre-check input data for validity
for (const SendCoinsRecipient &rcp : recipients) {
if (rcp.fSubtractFeeFromAmount) {
fSubtractFeeFromAmount = true;
}
#ifdef ENABLE_BIP70
// PaymentRequest...
if (rcp.paymentRequest.IsInitialized()) {
Amount subtotal = Amount::zero();
const payments::PaymentDetails &details =
rcp.paymentRequest.getDetails();
for (int i = 0; i < details.outputs_size(); i++) {
const payments::Output &out = details.outputs(i);
if (out.amount() <= 0) {
continue;
}
subtotal += int64_t(out.amount()) * SATOSHI;
const uint8_t *scriptStr = (const uint8_t *)out.script().data();
CScript scriptPubKey(scriptStr,
scriptStr + out.script().size());
Amount nAmount = int64_t(out.amount()) * SATOSHI;
CRecipient recipient = {scriptPubKey, nAmount,
rcp.fSubtractFeeFromAmount};
vecSend.push_back(recipient);
}
if (subtotal <= Amount::zero()) {
return InvalidAmount;
}
total += subtotal;
}
// User-entered bitcoin address / amount:
else
#endif
{
if (!validateAddress(rcp.address)) {
return InvalidAddress;
}
if (rcp.amount <= Amount::zero()) {
return InvalidAmount;
}
setAddress.insert(rcp.address);
++nAddresses;
CScript scriptPubKey = GetScriptForDestination(
DecodeDestination(rcp.address.toStdString(), getChainParams()));
CRecipient recipient = {scriptPubKey, Amount(rcp.amount),
rcp.fSubtractFeeFromAmount};
vecSend.push_back(recipient);
total += rcp.amount;
}
}
if (setAddress.size() != nAddresses) {
return DuplicateAddress;
}
Amount nBalance = m_wallet->getAvailableBalance(coinControl);
if (total > nBalance) {
return AmountExceedsBalance;
}
Amount nFeeRequired = Amount::zero();
int nChangePosRet = -1;
bilingual_str error;
auto &newTx = transaction.getWtx();
- newTx = m_wallet->createTransaction(
- vecSend, coinControl, !wallet().privateKeysDisabled() /* sign */,
- nChangePosRet, nFeeRequired, error);
+ const auto &res = m_wallet->createTransaction(
+ vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(),
+ nChangePosRet, nFeeRequired);
+ newTx = res ? *res : nullptr;
transaction.setTransactionFee(nFeeRequired);
if (fSubtractFeeFromAmount && newTx) {
transaction.reassignAmounts(nChangePosRet);
}
if (!newTx) {
if (!fSubtractFeeFromAmount && (total + nFeeRequired) > nBalance) {
return SendCoinsReturn(AmountWithFeeExceedsBalance);
}
- Q_EMIT message(tr("Send Coins"),
- QString::fromStdString(error.translated),
- CClientUIInterface::MSG_ERROR);
+ Q_EMIT message(
+ tr("Send Coins"),
+ QString::fromStdString(util::ErrorString(res).translated),
+ CClientUIInterface::MSG_ERROR);
return TransactionCreationFailed;
}
// Reject absurdly high fee. (This can never happen because the
// wallet never creates transactions with fee greater than
// m_default_max_tx_fee. This merely a belt-and-suspenders check).
if (nFeeRequired > m_wallet->getDefaultMaxTxFee()) {
return AbsurdFee;
}
return SendCoinsReturn(OK);
}
WalletModel::SendCoinsReturn
WalletModel::sendCoins(WalletModelTransaction &transaction) {
/* store serialized transaction */
QByteArray transaction_array;
std::vector<std::pair<std::string, std::string>> vOrderForm;
for (const SendCoinsRecipient &rcp : transaction.getRecipients()) {
#ifdef ENABLE_BIP70
if (rcp.paymentRequest.IsInitialized()) {
// Make sure any payment requests involved are still valid.
if (PaymentServer::verifyExpired(rcp.paymentRequest.getDetails())) {
return PaymentRequestExpired;
}
// Store PaymentRequests in wtx.vOrderForm in wallet.
std::string value;
rcp.paymentRequest.SerializeToString(&value);
vOrderForm.emplace_back("PaymentRequest", std::move(value));
} else
#endif
{
if (!rcp.message.isEmpty()) {
// Message from normal bitcoincash:URI
// (bitcoincash:123...?message=example)
vOrderForm.emplace_back("Message", rcp.message.toStdString());
}
}
}
auto &newTx = transaction.getWtx();
wallet().commitTransaction(newTx, {} /* mapValue */, std::move(vOrderForm));
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << *newTx;
transaction_array.append((const char *)ssTx.data(), ssTx.size());
// Add addresses / update labels that we've sent to the address book, and
// emit coinsSent signal for each recipient
for (const SendCoinsRecipient &rcp : transaction.getRecipients()) {
// Don't touch the address book when we have a payment request
#ifdef ENABLE_BIP70
if (!rcp.paymentRequest.IsInitialized())
#endif
{
std::string strAddress = rcp.address.toStdString();
CTxDestination dest =
DecodeDestination(strAddress, getChainParams());
std::string strLabel = rcp.label.toStdString();
// Check if we have a new address or an updated label
std::string name;
if (!m_wallet->getAddress(dest, &name, /* is_mine= */ nullptr,
/* purpose= */ nullptr)) {
m_wallet->setAddressBook(dest, strLabel, "send");
} else if (name != strLabel) {
// "" means don't change purpose
m_wallet->setAddressBook(dest, strLabel, "");
}
}
Q_EMIT coinsSent(this->wallet(), rcp, transaction_array);
}
// update balance immediately, otherwise there could be a short noticeable
// delay until pollBalanceChanged hits
checkBalanceChanged(m_wallet->getBalances());
return SendCoinsReturn(OK);
}
OptionsModel *WalletModel::getOptionsModel() {
return optionsModel;
}
AddressTableModel *WalletModel::getAddressTableModel() {
return addressTableModel;
}
TransactionTableModel *WalletModel::getTransactionTableModel() {
return transactionTableModel;
}
RecentRequestsTableModel *WalletModel::getRecentRequestsTableModel() {
return recentRequestsTableModel;
}
WalletModel::EncryptionStatus WalletModel::getEncryptionStatus() const {
if (!m_wallet->isCrypted()) {
return Unencrypted;
} else if (m_wallet->isLocked()) {
return Locked;
} else {
return Unlocked;
}
}
bool WalletModel::setWalletEncrypted(const SecureString &passphrase) {
return m_wallet->encryptWallet(passphrase);
}
bool WalletModel::setWalletLocked(bool locked, const SecureString &passPhrase) {
if (locked) {
// Lock
return m_wallet->lock();
} else {
// Unlock
return m_wallet->unlock(passPhrase);
}
}
bool WalletModel::changePassphrase(const SecureString &oldPass,
const SecureString &newPass) {
// Make sure wallet is locked before attempting pass change
m_wallet->lock();
return m_wallet->changeWalletPassphrase(oldPass, newPass);
}
// Handlers for core signals
static void NotifyUnload(WalletModel *walletModel) {
qDebug() << "NotifyUnload";
bool invoked = QMetaObject::invokeMethod(walletModel, "unload");
assert(invoked);
}
static void NotifyKeyStoreStatusChanged(WalletModel *walletmodel) {
qDebug() << "NotifyKeyStoreStatusChanged";
bool invoked = QMetaObject::invokeMethod(walletmodel, "updateStatus",
Qt::QueuedConnection);
assert(invoked);
}
static void NotifyAddressBookChanged(WalletModel *walletmodel,
const CTxDestination &address,
const std::string &label, bool isMine,
const std::string &purpose,
ChangeType status) {
QString strAddress = QString::fromStdString(
EncodeCashAddr(address, walletmodel->getChainParams()));
QString strLabel = QString::fromStdString(label);
QString strPurpose = QString::fromStdString(purpose);
qDebug() << "NotifyAddressBookChanged: " + strAddress + " " + strLabel +
" isMine=" + QString::number(isMine) +
" purpose=" + strPurpose +
" status=" + QString::number(status);
bool invoked = QMetaObject::invokeMethod(
walletmodel, "updateAddressBook", Qt::QueuedConnection,
Q_ARG(QString, strAddress), Q_ARG(QString, strLabel),
Q_ARG(bool, isMine), Q_ARG(QString, strPurpose), Q_ARG(int, status));
assert(invoked);
}
static void NotifyTransactionChanged(WalletModel *walletmodel, const TxId &hash,
ChangeType status) {
Q_UNUSED(hash);
Q_UNUSED(status);
bool invoked = QMetaObject::invokeMethod(walletmodel, "updateTransaction",
Qt::QueuedConnection);
assert(invoked);
}
static void ShowProgress(WalletModel *walletmodel, const std::string &title,
int nProgress) {
// emits signal "showProgress"
bool invoked = QMetaObject::invokeMethod(
walletmodel, "showProgress", Qt::QueuedConnection,
Q_ARG(QString, QString::fromStdString(title)), Q_ARG(int, nProgress));
assert(invoked);
}
static void NotifyWatchonlyChanged(WalletModel *walletmodel,
bool fHaveWatchonly) {
bool invoked = QMetaObject::invokeMethod(walletmodel, "updateWatchOnlyFlag",
Qt::QueuedConnection,
Q_ARG(bool, fHaveWatchonly));
assert(invoked);
}
static void NotifyCanGetAddressesChanged(WalletModel *walletmodel) {
bool invoked =
QMetaObject::invokeMethod(walletmodel, "canGetAddressesChanged");
assert(invoked);
}
void WalletModel::subscribeToCoreSignals() {
// Connect signals to wallet
m_handler_unload = m_wallet->handleUnload(std::bind(&NotifyUnload, this));
m_handler_status_changed = m_wallet->handleStatusChanged(
std::bind(&NotifyKeyStoreStatusChanged, this));
m_handler_address_book_changed = m_wallet->handleAddressBookChanged(
std::bind(NotifyAddressBookChanged, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3,
std::placeholders::_4, std::placeholders::_5));
m_handler_transaction_changed = m_wallet->handleTransactionChanged(
std::bind(NotifyTransactionChanged, this, std::placeholders::_1,
std::placeholders::_2));
m_handler_show_progress = m_wallet->handleShowProgress(std::bind(
ShowProgress, this, std::placeholders::_1, std::placeholders::_2));
m_handler_watch_only_changed = m_wallet->handleWatchOnlyChanged(
std::bind(NotifyWatchonlyChanged, this, std::placeholders::_1));
m_handler_can_get_addrs_changed = m_wallet->handleCanGetAddressesChanged(
std::bind(NotifyCanGetAddressesChanged, this));
}
void WalletModel::unsubscribeFromCoreSignals() {
// Disconnect signals from wallet
m_handler_unload->disconnect();
m_handler_status_changed->disconnect();
m_handler_address_book_changed->disconnect();
m_handler_transaction_changed->disconnect();
m_handler_show_progress->disconnect();
m_handler_watch_only_changed->disconnect();
m_handler_can_get_addrs_changed->disconnect();
}
// WalletModel::UnlockContext implementation
WalletModel::UnlockContext WalletModel::requestUnlock() {
bool was_locked = getEncryptionStatus() == Locked;
if (was_locked) {
// Request UI to unlock wallet
Q_EMIT requireUnlock();
}
// If wallet is still locked, unlock was failed or cancelled, mark context
// as invalid
bool valid = getEncryptionStatus() != Locked;
return UnlockContext(this, valid, was_locked);
}
WalletModel::UnlockContext::UnlockContext(WalletModel *_wallet, bool _valid,
bool _relock)
: wallet(_wallet), valid(_valid), relock(_relock) {}
WalletModel::UnlockContext::~UnlockContext() {
if (valid && relock) {
wallet->setWalletLocked(true);
}
}
void WalletModel::UnlockContext::CopyFrom(UnlockContext &&rhs) {
// Transfer context; old object no longer relocks wallet
*this = rhs;
rhs.relock = false;
}
void WalletModel::loadReceiveRequests(
std::vector<std::string> &vReceiveRequests) {
// receive request
vReceiveRequests = m_wallet->getDestValues("rr");
}
bool WalletModel::saveReceiveRequest(const std::string &sAddress,
const int64_t nId,
const std::string &sRequest) {
CTxDestination dest = DecodeDestination(sAddress, getChainParams());
std::stringstream ss;
ss << nId;
// "rr" prefix = "receive request" in destdata
std::string key = "rr" + ss.str();
return sRequest.empty() ? m_wallet->eraseDestData(dest, key)
: m_wallet->addDestData(dest, key, sRequest);
}
bool WalletModel::isWalletEnabled() {
return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET);
}
QString WalletModel::getWalletName() const {
return QString::fromStdString(m_wallet->getWalletName());
}
QString WalletModel::getDisplayName() const {
const QString name = getWalletName();
return name.isEmpty() ? "[" + tr("default wallet") + "]" : name;
}
bool WalletModel::isMultiwallet() {
return m_node.walletClient().getWallets().size() > 1;
}
const CChainParams &WalletModel::getChainParams() const {
return Params();
}
BlockHash WalletModel::getLastBlockProcessed() const {
return m_client_model ? m_client_model->getBestBlockHash() : BlockHash{};
}
diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp
index 4585f430a..f09f361ae 100644
--- a/src/wallet/interfaces.cpp
+++ b/src/wallet/interfaces.cpp
@@ -1,589 +1,588 @@
// Copyright (c) 2018 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <interfaces/wallet.h>
#include <chainparams.h>
#include <common/args.h>
#include <config.h>
#include <consensus/amount.h>
#include <consensus/validation.h>
#include <interfaces/chain.h>
#include <interfaces/handler.h>
#include <primitives/transaction.h>
#include <rpc/server.h>
#include <script/standard.h>
#include <support/allocators/secure.h>
#include <sync.h>
#include <util/check.h>
#include <util/ui_change_type.h>
#include <wallet/context.h>
#include <wallet/fees.h>
#include <wallet/ismine.h>
#include <wallet/load.h>
#include <wallet/receive.h>
#include <wallet/rpc/backup.h>
#include <wallet/rpc/encrypt.h>
#include <wallet/spend.h>
#include <wallet/wallet.h>
using interfaces::Chain;
using interfaces::FoundBlock;
using interfaces::Handler;
using interfaces::MakeHandler;
using interfaces::Wallet;
using interfaces::WalletAddress;
using interfaces::WalletBalances;
using interfaces::WalletClient;
using interfaces::WalletOrderForm;
using interfaces::WalletTx;
using interfaces::WalletTxOut;
using interfaces::WalletTxStatus;
using interfaces::WalletValueMap;
namespace wallet {
namespace {
//! Construct wallet tx struct.
WalletTx MakeWalletTx(CWallet &wallet, const CWalletTx &wtx) {
LOCK(wallet.cs_wallet);
WalletTx result;
result.tx = wtx.tx;
result.txin_is_mine.reserve(wtx.tx->vin.size());
for (const auto &txin : wtx.tx->vin) {
result.txin_is_mine.emplace_back(InputIsMine(wallet, txin));
}
result.txout_is_mine.reserve(wtx.tx->vout.size());
result.txout_address.reserve(wtx.tx->vout.size());
result.txout_address_is_mine.reserve(wtx.tx->vout.size());
for (const auto &txout : wtx.tx->vout) {
result.txout_is_mine.emplace_back(wallet.IsMine(txout));
result.txout_address.emplace_back();
result.txout_address_is_mine.emplace_back(
ExtractDestination(txout.scriptPubKey,
result.txout_address.back())
? wallet.IsMine(result.txout_address.back())
: ISMINE_NO);
}
result.credit = CachedTxGetCredit(wallet, wtx, ISMINE_ALL);
result.debit = CachedTxGetDebit(wallet, wtx, ISMINE_ALL);
result.change = CachedTxGetChange(wallet, wtx);
result.time = wtx.GetTxTime();
result.value_map = wtx.mapValue;
result.is_coinbase = wtx.IsCoinBase();
return result;
}
//! Construct wallet tx status struct.
WalletTxStatus MakeWalletTxStatus(const CWallet &wallet,
const CWalletTx &wtx)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) {
AssertLockHeld(wallet.cs_wallet);
WalletTxStatus result;
result.block_height = wtx.m_confirm.block_height > 0
? wtx.m_confirm.block_height
: std::numeric_limits<int>::max();
result.blocks_to_maturity = wallet.GetTxBlocksToMaturity(wtx);
result.depth_in_main_chain = wallet.GetTxDepthInMainChain(wtx);
result.time_received = wtx.nTimeReceived;
result.lock_time = wtx.tx->nLockTime;
result.is_trusted = CachedTxIsTrusted(wallet, wtx);
result.is_abandoned = wtx.isAbandoned();
result.is_coinbase = wtx.IsCoinBase();
result.is_in_main_chain = wallet.IsTxInMainChain(wtx);
return result;
}
//! Construct wallet TxOut struct.
WalletTxOut MakeWalletTxOut(const CWallet &wallet, const CWalletTx &wtx,
int n, int depth)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) {
AssertLockHeld(wallet.cs_wallet);
WalletTxOut result;
result.txout = wtx.tx->vout[n];
result.time = wtx.GetTxTime();
result.depth_in_main_chain = depth;
result.is_spent = wallet.IsSpent(COutPoint(wtx.GetId(), n));
return result;
}
class WalletImpl : public Wallet {
public:
explicit WalletImpl(const std::shared_ptr<CWallet> &wallet)
: m_wallet(wallet) {}
bool encryptWallet(const SecureString &wallet_passphrase) override {
return m_wallet->EncryptWallet(wallet_passphrase);
}
bool isCrypted() override { return m_wallet->IsCrypted(); }
bool lock() override { return m_wallet->Lock(); }
bool unlock(const SecureString &wallet_passphrase) override {
return m_wallet->Unlock(wallet_passphrase);
}
bool isLocked() override { return m_wallet->IsLocked(); }
bool changeWalletPassphrase(
const SecureString &old_wallet_passphrase,
const SecureString &new_wallet_passphrase) override {
return m_wallet->ChangeWalletPassphrase(old_wallet_passphrase,
new_wallet_passphrase);
}
void abortRescan() override { m_wallet->AbortRescan(); }
bool backupWallet(const std::string &filename) override {
return m_wallet->BackupWallet(filename);
}
std::string getWalletName() override { return m_wallet->GetName(); }
bool getNewDestination(const OutputType type, const std::string label,
CTxDestination &dest) override {
LOCK(m_wallet->cs_wallet);
std::string error;
return m_wallet->GetNewDestination(type, label, dest, error);
}
const CChainParams &getChainParams() override {
return m_wallet->GetChainParams();
}
bool getPubKey(const CScript &script, const CKeyID &address,
CPubKey &pub_key) override {
std::unique_ptr<SigningProvider> provider =
m_wallet->GetSolvingProvider(script);
if (provider) {
return provider->GetPubKey(address, pub_key);
}
return false;
}
SigningResult signMessage(const std::string &message,
const PKHash &pkhash,
std::string &str_sig) override {
return m_wallet->SignMessage(message, pkhash, str_sig);
}
bool isSpendable(const CTxDestination &dest) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->IsMine(dest) & ISMINE_SPENDABLE;
}
bool haveWatchOnly() override {
auto spk_man = m_wallet->GetLegacyScriptPubKeyMan();
if (spk_man) {
return spk_man->HaveWatchOnly();
}
return false;
};
bool setAddressBook(const CTxDestination &dest, const std::string &name,
const std::string &purpose) override {
return m_wallet->SetAddressBook(dest, name, purpose);
}
bool delAddressBook(const CTxDestination &dest) override {
return m_wallet->DelAddressBook(dest);
}
bool getAddress(const CTxDestination &dest, std::string *name,
isminetype *is_mine, std::string *purpose) override {
LOCK(m_wallet->cs_wallet);
auto it = m_wallet->m_address_book.find(dest);
if (it == m_wallet->m_address_book.end() || it->second.IsChange()) {
return false;
}
if (name) {
*name = it->second.GetLabel();
}
if (is_mine) {
*is_mine = m_wallet->IsMine(dest);
}
if (purpose) {
*purpose = it->second.purpose;
}
return true;
}
std::vector<WalletAddress> getAddresses() override {
LOCK(m_wallet->cs_wallet);
std::vector<WalletAddress> result;
for (const auto &item : m_wallet->m_address_book) {
if (item.second.IsChange()) {
continue;
}
result.emplace_back(item.first, m_wallet->IsMine(item.first),
item.second.GetLabel(),
item.second.purpose);
}
return result;
}
bool addDestData(const CTxDestination &dest, const std::string &key,
const std::string &value) override {
LOCK(m_wallet->cs_wallet);
WalletBatch batch{m_wallet->GetDatabase()};
return m_wallet->AddDestData(batch, dest, key, value);
}
bool eraseDestData(const CTxDestination &dest,
const std::string &key) override {
LOCK(m_wallet->cs_wallet);
WalletBatch batch{m_wallet->GetDatabase()};
return m_wallet->EraseDestData(batch, dest, key);
}
std::vector<std::string>
getDestValues(const std::string &prefix) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->GetDestValues(prefix);
}
void lockCoin(const COutPoint &output) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->LockCoin(output);
}
void unlockCoin(const COutPoint &output) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->UnlockCoin(output);
}
bool isLockedCoin(const COutPoint &output) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->IsLockedCoin(output);
}
void listLockedCoins(std::vector<COutPoint> &outputs) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->ListLockedCoins(outputs);
}
- CTransactionRef
+ util::Result<CTransactionRef>
createTransaction(const std::vector<CRecipient> &recipients,
const CCoinControl &coin_control, bool sign,
- int &change_pos, Amount &fee,
- bilingual_str &fail_reason) override {
+ int &change_pos, Amount &fee) override {
LOCK(m_wallet->cs_wallet);
- std::optional<CreatedTransactionResult> txr =
- CreateTransaction(*m_wallet, recipients, change_pos,
- fail_reason, coin_control, sign);
- if (!txr) {
- return {};
+ auto res = CreateTransaction(*m_wallet, recipients, change_pos,
+ coin_control, sign);
+ if (!res) {
+ return util::Error{util::ErrorString(res)};
}
- fee = txr->fee;
- change_pos = txr->change_pos;
+ const auto &txr = *res;
+ fee = txr.fee;
+ change_pos = txr.change_pos;
- return txr->tx;
+ return txr.tx;
}
void commitTransaction(CTransactionRef tx, WalletValueMap value_map,
WalletOrderForm order_form) override {
LOCK(m_wallet->cs_wallet);
m_wallet->CommitTransaction(std::move(tx), std::move(value_map),
std::move(order_form));
}
bool transactionCanBeAbandoned(const TxId &txid) override {
return m_wallet->TransactionCanBeAbandoned(txid);
}
bool abandonTransaction(const TxId &txid) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->AbandonTransaction(txid);
}
CTransactionRef getTx(const TxId &txid) override {
LOCK(m_wallet->cs_wallet);
auto mi = m_wallet->mapWallet.find(txid);
if (mi != m_wallet->mapWallet.end()) {
return mi->second.tx;
}
return {};
}
WalletTx getWalletTx(const TxId &txid) override {
LOCK(m_wallet->cs_wallet);
auto mi = m_wallet->mapWallet.find(txid);
if (mi != m_wallet->mapWallet.end()) {
return MakeWalletTx(*m_wallet, mi->second);
}
return {};
}
std::vector<WalletTx> getWalletTxs() override {
LOCK(m_wallet->cs_wallet);
std::vector<WalletTx> result;
result.reserve(m_wallet->mapWallet.size());
for (const auto &entry : m_wallet->mapWallet) {
result.emplace_back(MakeWalletTx(*m_wallet, entry.second));
}
return result;
}
bool tryGetTxStatus(const TxId &txid,
interfaces::WalletTxStatus &tx_status,
int &num_blocks, int64_t &block_time) override {
TRY_LOCK(m_wallet->cs_wallet, locked_wallet);
if (!locked_wallet) {
return false;
}
auto mi = m_wallet->mapWallet.find(txid);
if (mi == m_wallet->mapWallet.end()) {
return false;
}
num_blocks = m_wallet->GetLastBlockHeight();
block_time = -1;
CHECK_NONFATAL(m_wallet->chain().findBlock(
m_wallet->GetLastBlockHash(), FoundBlock().time(block_time)));
tx_status = MakeWalletTxStatus(*m_wallet, mi->second);
return true;
}
WalletTx getWalletTxDetails(const TxId &txid, WalletTxStatus &tx_status,
WalletOrderForm &order_form,
bool &in_mempool,
int &num_blocks) override {
LOCK(m_wallet->cs_wallet);
auto mi = m_wallet->mapWallet.find(txid);
if (mi != m_wallet->mapWallet.end()) {
num_blocks = m_wallet->GetLastBlockHeight();
in_mempool = mi->second.InMempool();
order_form = mi->second.vOrderForm;
tx_status = MakeWalletTxStatus(*m_wallet, mi->second);
return MakeWalletTx(*m_wallet, mi->second);
}
return {};
}
TransactionError fillPSBT(SigHashType sighash_type, bool sign,
bool bip32derivs,
PartiallySignedTransaction &psbtx,
bool &complete) const override {
return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign,
bip32derivs);
}
WalletBalances getBalances() override {
const auto bal = GetBalance(*m_wallet);
WalletBalances result;
result.balance = bal.m_mine_trusted;
result.unconfirmed_balance = bal.m_mine_untrusted_pending;
result.immature_balance = bal.m_mine_immature;
result.have_watch_only = haveWatchOnly();
if (result.have_watch_only) {
result.watch_only_balance = bal.m_watchonly_trusted;
result.unconfirmed_watch_only_balance =
bal.m_watchonly_untrusted_pending;
result.immature_watch_only_balance = bal.m_watchonly_immature;
}
return result;
}
bool tryGetBalances(WalletBalances &balances,
BlockHash &block_hash) override {
TRY_LOCK(m_wallet->cs_wallet, locked_wallet);
if (!locked_wallet) {
return false;
}
block_hash = m_wallet->GetLastBlockHash();
balances = getBalances();
return true;
}
Amount getBalance() override {
return GetBalance(*m_wallet).m_mine_trusted;
}
Amount getAvailableBalance(const CCoinControl &coin_control) override {
return GetAvailableBalance(*m_wallet, &coin_control);
}
isminetype txinIsMine(const CTxIn &txin) override {
LOCK(m_wallet->cs_wallet);
return InputIsMine(*m_wallet, txin);
}
isminetype txoutIsMine(const CTxOut &txout) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->IsMine(txout);
}
Amount getDebit(const CTxIn &txin, isminefilter filter) override {
LOCK(m_wallet->cs_wallet);
return m_wallet->GetDebit(txin, filter);
}
Amount getCredit(const CTxOut &txout, isminefilter filter) override {
LOCK(m_wallet->cs_wallet);
return OutputGetCredit(*m_wallet, txout, filter);
}
CoinsList listCoins() override {
LOCK(m_wallet->cs_wallet);
CoinsList result;
for (const auto &entry : ListCoins(*m_wallet)) {
auto &group = result[entry.first];
for (const auto &coin : entry.second) {
group.emplace_back(COutPoint(coin.tx->GetId(), coin.i),
MakeWalletTxOut(*m_wallet, *coin.tx,
coin.i, coin.nDepth));
}
}
return result;
}
std::vector<WalletTxOut>
getCoins(const std::vector<COutPoint> &outputs) override {
LOCK(m_wallet->cs_wallet);
std::vector<WalletTxOut> result;
result.reserve(outputs.size());
for (const auto &output : outputs) {
result.emplace_back();
auto it = m_wallet->mapWallet.find(output.GetTxId());
if (it != m_wallet->mapWallet.end()) {
int depth = m_wallet->GetTxDepthInMainChain(it->second);
if (depth >= 0) {
result.back() = MakeWalletTxOut(*m_wallet, it->second,
output.GetN(), depth);
}
}
}
return result;
}
bool hdEnabled() override { return m_wallet->IsHDEnabled(); }
OutputType getDefaultAddressType() override {
return m_wallet->m_default_address_type;
}
bool canGetAddresses() const override {
return m_wallet->CanGetAddresses();
}
bool privateKeysDisabled() override {
return m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
}
Amount getDefaultMaxTxFee() override {
return m_wallet->m_default_max_tx_fee;
}
void remove() override {
RemoveWallet(m_wallet, false /* load_on_start */);
}
bool isLegacy() override { return m_wallet->IsLegacy(); }
std::unique_ptr<Handler> handleUnload(UnloadFn fn) override {
return MakeHandler(m_wallet->NotifyUnload.connect(fn));
}
std::unique_ptr<Handler>
handleShowProgress(ShowProgressFn fn) override {
return MakeHandler(m_wallet->ShowProgress.connect(fn));
}
std::unique_ptr<Handler>
handleStatusChanged(StatusChangedFn fn) override {
return MakeHandler(m_wallet->NotifyStatusChanged.connect(
[fn](CWallet *) { fn(); }));
}
std::unique_ptr<Handler>
handleAddressBookChanged(AddressBookChangedFn fn) override {
return MakeHandler(m_wallet->NotifyAddressBookChanged.connect(
[fn](CWallet *, const CTxDestination &address,
const std::string &label, bool is_mine,
const std::string &purpose, ChangeType status) {
fn(address, label, is_mine, purpose, status);
}));
}
std::unique_ptr<Handler>
handleTransactionChanged(TransactionChangedFn fn) override {
return MakeHandler(m_wallet->NotifyTransactionChanged.connect(
[fn](CWallet *, const TxId &txid, ChangeType status) {
fn(txid, status);
}));
}
std::unique_ptr<Handler>
handleWatchOnlyChanged(WatchOnlyChangedFn fn) override {
return MakeHandler(m_wallet->NotifyWatchonlyChanged.connect(fn));
}
std::unique_ptr<Handler>
handleCanGetAddressesChanged(CanGetAddressesChangedFn fn) override {
return MakeHandler(
m_wallet->NotifyCanGetAddressesChanged.connect(fn));
}
Amount getRequiredFee(unsigned int tx_bytes) override {
return GetRequiredFee(*m_wallet, tx_bytes);
}
Amount getMinimumFee(unsigned int tx_bytes,
const CCoinControl &coin_control) override {
return GetMinimumFee(*m_wallet, tx_bytes, coin_control);
}
CWallet *wallet() override { return m_wallet.get(); }
std::shared_ptr<CWallet> m_wallet;
};
class WalletClientImpl : public WalletClient {
public:
WalletClientImpl(Chain &chain, ArgsManager &args) {
m_context.chain = &chain;
m_context.args = &args;
}
~WalletClientImpl() override { UnloadWallets(); }
//! ChainClient methods
void registerRpcs(const Span<const CRPCCommand> &commands) {
for (const CRPCCommand &command : commands) {
m_rpc_commands.emplace_back(
command.category, command.name,
[this, &command](const Config &config,
const JSONRPCRequest &request,
UniValue &result, bool last_handler) {
JSONRPCRequest wallet_request = request;
wallet_request.context = &m_context;
return command.actor(config, wallet_request, result,
last_handler);
},
command.argNames, command.unique_id);
m_rpc_handlers.emplace_back(
m_context.chain->handleRpc(m_rpc_commands.back()));
}
}
void registerRpcs() override {
registerRpcs(GetWalletRPCCommands());
registerRpcs(GetWalletDumpRPCCommands());
registerRpcs(GetWalletEncryptRPCCommands());
}
bool verify() override { return VerifyWallets(*m_context.chain); }
bool load() override { return LoadWallets(*m_context.chain); }
void start(CScheduler &scheduler) override {
return StartWallets(scheduler, *Assert(m_context.args));
}
void flush() override { return FlushWallets(); }
void stop() override { return StopWallets(); }
void setMockTime(int64_t time) override { return SetMockTime(time); }
//! WalletClient methods
std::unique_ptr<Wallet>
createWallet(const std::string &name, const SecureString &passphrase,
uint64_t wallet_creation_flags, bilingual_str &error,
std::vector<bilingual_str> &warnings) override {
std::shared_ptr<CWallet> wallet;
DatabaseOptions options;
DatabaseStatus status;
options.require_create = true;
options.create_flags = wallet_creation_flags;
options.create_passphrase = passphrase;
return MakeWallet(CreateWallet(*m_context.chain, name,
true /* load_on_start */, options,
status, error, warnings));
}
std::unique_ptr<Wallet>
loadWallet(const std::string &name, bilingual_str &error,
std::vector<bilingual_str> &warnings) override {
DatabaseOptions options;
DatabaseStatus status;
options.require_existing = true;
return MakeWallet(LoadWallet(*m_context.chain, name,
true /* load_on_start */, options,
status, error, warnings));
}
std::string getWalletDir() override {
return fs::PathToString(GetWalletDir());
}
std::vector<std::string> listWalletDir() override {
std::vector<std::string> paths;
for (auto &path : ListWalletDir()) {
paths.push_back(fs::PathToString(path));
}
return paths;
}
std::vector<std::unique_ptr<Wallet>> getWallets() override {
std::vector<std::unique_ptr<Wallet>> wallets;
for (const auto &wallet : GetWallets()) {
wallets.emplace_back(MakeWallet(wallet));
}
return wallets;
}
std::unique_ptr<Handler> handleLoadWallet(LoadWalletFn fn) override {
return HandleLoadWallet(std::move(fn));
}
WalletContext m_context;
const std::vector<std::string> m_wallet_filenames;
std::vector<std::unique_ptr<Handler>> m_rpc_handlers;
std::list<CRPCCommand> m_rpc_commands;
};
} // namespace
} // namespace wallet
namespace interfaces {
std::unique_ptr<Wallet> MakeWallet(const std::shared_ptr<CWallet> &wallet) {
return wallet ? std::make_unique<wallet::WalletImpl>(wallet) : nullptr;
}
std::unique_ptr<WalletClient> MakeWalletClient(Chain &chain,
ArgsManager &args) {
return std::make_unique<wallet::WalletClientImpl>(chain, args);
}
} // namespace interfaces
diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp
index 9604245ae..22ed220c7 100644
--- a/src/wallet/rpcwallet.cpp
+++ b/src/wallet/rpcwallet.cpp
@@ -1,4976 +1,4977 @@
// Copyright (c) 2010 Satoshi Nakamoto
// Copyright (c) 2009-2019 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chainparams.h> // for GetConsensus.
#include <coins.h>
#include <common/system.h>
#include <config.h>
#include <consensus/amount.h>
#include <consensus/validation.h>
#include <core_io.h>
#include <interfaces/chain.h>
#include <key_io.h>
#include <node/context.h>
#include <outputtype.h>
#include <policy/fees.h>
#include <policy/policy.h>
#include <rpc/rawtransaction_util.h>
#include <rpc/server.h>
#include <rpc/util.h>
#include <script/descriptor.h>
#include <util/bip32.h>
#include <util/error.h>
#include <util/moneystr.h>
+#include <util/result.h>
#include <util/string.h>
#include <util/translation.h>
#include <util/url.h>
#include <util/vector.h>
#include <wallet/coincontrol.h>
#include <wallet/context.h>
#include <wallet/load.h>
#include <wallet/receive.h>
#include <wallet/rpc/util.h>
#include <wallet/rpcwallet.h>
#include <wallet/spend.h>
#include <wallet/wallet.h>
#include <wallet/walletdb.h>
#include <wallet/walletutil.h>
#include <univalue.h>
#include <event2/http.h>
#include <optional>
#include <variant>
using interfaces::FoundBlock;
/**
* Checks if a CKey is in the given CWallet compressed or otherwise
*/
bool HaveKey(const SigningProvider &wallet, const CKey &key) {
CKey key2;
key2.Set(key.begin(), key.end(), !key.IsCompressed());
return wallet.HaveKey(key.GetPubKey().GetID()) ||
wallet.HaveKey(key2.GetPubKey().GetID());
}
static void WalletTxToJSON(const CWallet &wallet, const CWalletTx &wtx,
UniValue &entry)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) {
interfaces::Chain &chain = wallet.chain();
int confirms = wallet.GetTxDepthInMainChain(wtx);
entry.pushKV("confirmations", confirms);
if (wtx.IsCoinBase()) {
entry.pushKV("generated", true);
}
if (confirms > 0) {
entry.pushKV("blockhash", wtx.m_confirm.hashBlock.GetHex());
entry.pushKV("blockheight", wtx.m_confirm.block_height);
entry.pushKV("blockindex", wtx.m_confirm.nIndex);
int64_t block_time;
CHECK_NONFATAL(chain.findBlock(wtx.m_confirm.hashBlock,
FoundBlock().time(block_time)));
entry.pushKV("blocktime", block_time);
} else {
entry.pushKV("trusted", CachedTxIsTrusted(wallet, wtx));
}
uint256 hash = wtx.GetId();
entry.pushKV("txid", hash.GetHex());
UniValue conflicts(UniValue::VARR);
for (const uint256 &conflict : wallet.GetTxConflicts(wtx)) {
conflicts.push_back(conflict.GetHex());
}
entry.pushKV("walletconflicts", conflicts);
entry.pushKV("time", wtx.GetTxTime());
entry.pushKV("timereceived", int64_t{wtx.nTimeReceived});
for (const std::pair<const std::string, std::string> &item : wtx.mapValue) {
entry.pushKV(item.first, item.second);
}
}
static RPCHelpMan getnewaddress() {
return RPCHelpMan{
"getnewaddress",
"Returns a new eCash address for receiving payments.\n"
"If 'label' is specified, it is added to the address book \n"
"so payments received with the address will be associated with "
"'label'.\n",
{
{"label", RPCArg::Type::STR, RPCArg::Default{""},
"The label name for the address to be linked to. If not provided, "
"the default label \"\" is used. It can also be set to the empty "
"string \"\" to represent the default label. The label does not "
"need to exist, it will be created if there is no label by the "
"given name."},
},
RPCResult{RPCResult::Type::STR, "address", "The new eCash address"},
RPCExamples{HelpExampleCli("getnewaddress", "") +
HelpExampleRpc("getnewaddress", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
if (!pwallet->CanGetAddresses()) {
throw JSONRPCError(RPC_WALLET_ERROR,
"Error: This wallet has no available keys");
}
// Parse the label first so we don't generate a key if there's an
// error
std::string label;
if (!request.params[0].isNull()) {
label = LabelFromValue(request.params[0]);
}
CTxDestination dest;
std::string error;
if (!pwallet->GetNewDestination(OutputType::LEGACY, label, dest,
error)) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error);
}
return EncodeDestination(dest, config);
},
};
}
static RPCHelpMan getrawchangeaddress() {
return RPCHelpMan{
"getrawchangeaddress",
"Returns a new Bitcoin address, for receiving change.\n"
"This is for use with raw transactions, NOT normal use.\n",
{},
RPCResult{RPCResult::Type::STR, "address", "The address"},
RPCExamples{HelpExampleCli("getrawchangeaddress", "") +
HelpExampleRpc("getrawchangeaddress", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
if (!pwallet->CanGetAddresses(true)) {
throw JSONRPCError(RPC_WALLET_ERROR,
"Error: This wallet has no available keys");
}
OutputType output_type = pwallet->m_default_change_type.value_or(
pwallet->m_default_address_type);
if (!request.params[0].isNull()) {
if (!ParseOutputType(request.params[0].get_str(),
output_type)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
strprintf("Unknown address type '%s'",
request.params[0].get_str()));
}
}
CTxDestination dest;
std::string error;
if (!pwallet->GetNewChangeDestination(output_type, dest, error)) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, error);
}
return EncodeDestination(dest, config);
},
};
}
static RPCHelpMan setlabel() {
return RPCHelpMan{
"setlabel",
"Sets the label associated with the given address.\n",
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO,
"The bitcoin address to be associated with a label."},
{"label", RPCArg::Type::STR, RPCArg::Optional::NO,
"The label to assign to the address."},
},
RPCResult{RPCResult::Type::NONE, "", ""},
RPCExamples{
HelpExampleCli("setlabel",
"\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\" \"tabby\"") +
HelpExampleRpc(
"setlabel",
"\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\", \"tabby\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
CTxDestination dest = DecodeDestination(request.params[0].get_str(),
wallet->GetChainParams());
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Invalid Bitcoin address");
}
std::string label = LabelFromValue(request.params[1]);
if (pwallet->IsMine(dest)) {
pwallet->SetAddressBook(dest, label, "receive");
} else {
pwallet->SetAddressBook(dest, label, "send");
}
return NullUniValue;
},
};
}
void ParseRecipients(const UniValue &address_amounts,
const UniValue &subtract_fee_outputs,
std::vector<CRecipient> &recipients,
const CChainParams &chainParams) {
std::set<CTxDestination> destinations;
int i = 0;
for (const std::string &address : address_amounts.getKeys()) {
CTxDestination dest = DecodeDestination(address, chainParams);
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
std::string("Invalid Bitcoin address: ") +
address);
}
if (destinations.count(dest)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
std::string("Invalid parameter, duplicated address: ") +
address);
}
destinations.insert(dest);
CScript script_pub_key = GetScriptForDestination(dest);
Amount amount = AmountFromValue(address_amounts[i++]);
bool subtract_fee = false;
for (unsigned int idx = 0; idx < subtract_fee_outputs.size(); idx++) {
const UniValue &addr = subtract_fee_outputs[idx];
if (addr.get_str() == address) {
subtract_fee = true;
}
}
CRecipient recipient = {script_pub_key, amount, subtract_fee};
recipients.push_back(recipient);
}
}
UniValue SendMoney(CWallet *const pwallet, const CCoinControl &coin_control,
std::vector<CRecipient> &recipients, mapValue_t map_value,
bool broadcast = true) {
EnsureWalletIsUnlocked(pwallet);
// Shuffle recipient list
std::shuffle(recipients.begin(), recipients.end(), FastRandomContext());
// Send
constexpr int RANDOM_CHANGE_POSITION = -1;
- bilingual_str error;
- std::optional<CreatedTransactionResult> txr = CreateTransaction(
- *pwallet, recipients, RANDOM_CHANGE_POSITION, error, coin_control,
+ auto res = CreateTransaction(
+ *pwallet, recipients, RANDOM_CHANGE_POSITION, coin_control,
!pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
- if (!txr) {
- throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, error.original);
+ if (!res) {
+ throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS,
+ util::ErrorString(res).original);
}
- CTransactionRef tx = txr->tx;
- pwallet->CommitTransaction(tx, std::move(map_value), {} /* orderForm */,
+ const CTransactionRef &tx = res->tx;
+ pwallet->CommitTransaction(tx, std::move(map_value), /*orderForm=*/{},
broadcast);
return tx->GetId().GetHex();
}
static RPCHelpMan sendtoaddress() {
return RPCHelpMan{
"sendtoaddress",
"Send an amount to a given address.\n" + HELP_REQUIRING_PASSPHRASE,
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO,
"The bitcoin address to send to."},
{"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO,
"The amount in " + Currency::get().ticker + " to send. eg 0.1"},
{"comment", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"A comment used to store what the transaction is for.\n"
" This is not part of the "
"transaction, just kept in your wallet."},
{"comment_to", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"A comment to store the name of the person or organization\n"
" to which you're sending the "
"transaction. This is not part of the \n"
" transaction, just kept in "
"your wallet."},
{"subtractfeefromamount", RPCArg::Type::BOOL,
RPCArg::Default{false},
"The fee will be deducted from the amount being sent.\n"
" The recipient will receive "
"less bitcoins than you enter in the amount field."},
{"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{true},
"(only available if avoid_reuse wallet flag is set) Avoid "
"spending from dirty addresses; addresses are considered\n"
" dirty if they have previously "
"been used in a transaction."},
},
RPCResult{RPCResult::Type::STR_HEX, "txid", "The transaction id."},
RPCExamples{
HelpExampleCli("sendtoaddress",
"\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 100000") +
HelpExampleCli("sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvay"
"dd\" 100000 \"donation\" \"seans "
"outpost\"") +
HelpExampleCli("sendtoaddress", "\"1M72Sfpbz1BPpXFHz9m3CdqATR44"
"Jvaydd\" 100000 \"\" \"\" true") +
HelpExampleRpc("sendtoaddress",
"\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvay"
"dd\", 100000, \"donation\", \"seans "
"outpost\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
// Wallet comments
mapValue_t mapValue;
if (!request.params[2].isNull() &&
!request.params[2].get_str().empty()) {
mapValue["comment"] = request.params[2].get_str();
}
if (!request.params[3].isNull() &&
!request.params[3].get_str().empty()) {
mapValue["to"] = request.params[3].get_str();
}
bool fSubtractFeeFromAmount = false;
if (!request.params[4].isNull()) {
fSubtractFeeFromAmount = request.params[4].get_bool();
}
CCoinControl coin_control;
coin_control.m_avoid_address_reuse =
GetAvoidReuseFlag(pwallet, request.params[5]);
// We also enable partial spend avoidance if reuse avoidance is set.
coin_control.m_avoid_partial_spends |=
coin_control.m_avoid_address_reuse;
EnsureWalletIsUnlocked(pwallet);
UniValue address_amounts(UniValue::VOBJ);
const std::string address = request.params[0].get_str();
address_amounts.pushKV(address, request.params[1]);
UniValue subtractFeeFromAmount(UniValue::VARR);
if (fSubtractFeeFromAmount) {
subtractFeeFromAmount.push_back(address);
}
std::vector<CRecipient> recipients;
ParseRecipients(address_amounts, subtractFeeFromAmount, recipients,
wallet->GetChainParams());
return SendMoney(pwallet, coin_control, recipients, mapValue);
},
};
}
static RPCHelpMan listaddressgroupings() {
return RPCHelpMan{
"listaddressgroupings",
"Lists groups of addresses which have had their common ownership\n"
"made public by common use as inputs or as the resulting change\n"
"in past transactions\n",
{},
RPCResult{RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::ARR_FIXED,
"",
"",
{
{RPCResult::Type::STR, "address",
"The bitcoin address"},
{RPCResult::Type::STR_AMOUNT, "amount",
"The amount in " + Currency::get().ticker},
{RPCResult::Type::STR, "label",
/* optional */ true, "The label"},
}},
}},
}},
RPCExamples{HelpExampleCli("listaddressgroupings", "") +
HelpExampleRpc("listaddressgroupings", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
UniValue jsonGroupings(UniValue::VARR);
std::map<CTxDestination, Amount> balances =
GetAddressBalances(*pwallet);
for (const std::set<CTxDestination> &grouping :
GetAddressGroupings(*pwallet)) {
UniValue jsonGrouping(UniValue::VARR);
for (const CTxDestination &address : grouping) {
UniValue addressInfo(UniValue::VARR);
addressInfo.push_back(EncodeDestination(address, config));
addressInfo.push_back(balances[address]);
const auto *address_book_entry =
pwallet->FindAddressBookEntry(address);
if (address_book_entry) {
addressInfo.push_back(address_book_entry->GetLabel());
}
jsonGrouping.push_back(addressInfo);
}
jsonGroupings.push_back(jsonGrouping);
}
return jsonGroupings;
},
};
}
static Amount GetReceived(const CWallet &wallet, const UniValue &params,
bool by_label)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) {
std::set<CTxDestination> address_set;
if (by_label) {
// Get the set of addresses assigned to label
std::string label = LabelFromValue(params[0]);
address_set = wallet.GetLabelAddresses(label);
} else {
// Get the address
CTxDestination dest =
DecodeDestination(params[0].get_str(), wallet.GetChainParams());
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Invalid Bitcoin address");
}
CScript script_pub_key = GetScriptForDestination(dest);
if (!wallet.IsMine(script_pub_key)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Address not found in wallet");
}
address_set.insert(dest);
}
// Minimum confirmations
int min_depth = 1;
if (!params[1].isNull()) {
min_depth = params[1].getInt<int>();
}
// Tally
Amount amount = Amount::zero();
for (const std::pair<const TxId, CWalletTx> &wtx_pair : wallet.mapWallet) {
const CWalletTx &wtx = wtx_pair.second;
if (wtx.IsCoinBase() || wallet.GetTxDepthInMainChain(wtx) < min_depth) {
continue;
}
for (const CTxOut &txout : wtx.tx->vout) {
CTxDestination address;
if (ExtractDestination(txout.scriptPubKey, address) &&
wallet.IsMine(address) && address_set.count(address)) {
amount += txout.nValue;
}
}
}
return amount;
}
static RPCHelpMan getreceivedbyaddress() {
return RPCHelpMan{
"getreceivedbyaddress",
"Returns the total amount received by the given address in "
"transactions with at least minconf confirmations.\n",
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO,
"The bitcoin address for transactions."},
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1},
"Only include transactions confirmed at least this many times."},
},
RPCResult{RPCResult::Type::STR_AMOUNT, "amount",
"The total amount in " + Currency::get().ticker +
" received at this address."},
RPCExamples{
"\nThe amount from transactions with at least 1 confirmation\n" +
HelpExampleCli("getreceivedbyaddress",
"\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\"") +
"\nThe amount including unconfirmed transactions, zero "
"confirmations\n" +
HelpExampleCli("getreceivedbyaddress",
"\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\" 0") +
"\nThe amount with at least 6 confirmations\n" +
HelpExampleCli("getreceivedbyaddress",
"\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\" 6") +
"\nAs a JSON-RPC call\n" +
HelpExampleRpc("getreceivedbyaddress",
"\"1D1ZrZNe3JUo7ZycKEYQQiQAWd9y54F4XX\", 6")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
return GetReceived(*pwallet, request.params,
/* by_label */ false);
},
};
}
static RPCHelpMan getreceivedbylabel() {
return RPCHelpMan{
"getreceivedbylabel",
"Returns the total amount received by addresses with <label> in "
"transactions with at least [minconf] confirmations.\n",
{
{"label", RPCArg::Type::STR, RPCArg::Optional::NO,
"The selected label, may be the default label using \"\"."},
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1},
"Only include transactions confirmed at least this many times."},
},
RPCResult{RPCResult::Type::STR_AMOUNT, "amount",
"The total amount in " + Currency::get().ticker +
" received for this label."},
RPCExamples{"\nAmount received by the default label with at least 1 "
"confirmation\n" +
HelpExampleCli("getreceivedbylabel", "\"\"") +
"\nAmount received at the tabby label including "
"unconfirmed amounts with zero confirmations\n" +
HelpExampleCli("getreceivedbylabel", "\"tabby\" 0") +
"\nThe amount with at least 6 confirmations\n" +
HelpExampleCli("getreceivedbylabel", "\"tabby\" 6") +
"\nAs a JSON-RPC call\n" +
HelpExampleRpc("getreceivedbylabel", "\"tabby\", 6")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
return GetReceived(*pwallet, request.params,
/* by_label */ true);
},
};
}
static RPCHelpMan getbalance() {
return RPCHelpMan{
"getbalance",
"Returns the total available balance.\n"
"The available balance is what the wallet considers currently "
"spendable, and is\n"
"thus affected by options which limit spendability such as "
"-spendzeroconfchange.\n",
{
{"dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"Remains for backward compatibility. Must be excluded or set to "
"\"*\"."},
{"minconf", RPCArg::Type::NUM, RPCArg::Default{0},
"Only include transactions confirmed at least this many times."},
{"include_watchonly", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Also include balance in watch-only addresses (see "
"'importaddress')"},
{"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{true},
"(only available if avoid_reuse wallet flag is set) Do not "
"include balance in dirty outputs; addresses are considered dirty "
"if they have previously been used in a transaction."},
},
RPCResult{RPCResult::Type::STR_AMOUNT, "amount",
"The total amount in " + Currency::get().ticker +
" received for this wallet."},
RPCExamples{
"\nThe total amount in the wallet with 0 or more confirmations\n" +
HelpExampleCli("getbalance", "") +
"\nThe total amount in the wallet with at least 6 confirmations\n" +
HelpExampleCli("getbalance", "\"*\" 6") + "\nAs a JSON-RPC call\n" +
HelpExampleRpc("getbalance", "\"*\", 6")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
const auto dummy_value{self.MaybeArg<std::string>("dummy")};
if (dummy_value && *dummy_value != "*") {
throw JSONRPCError(
RPC_METHOD_DEPRECATED,
"dummy first argument must be excluded or set to \"*\".");
}
const auto min_depth{self.Arg<int>("minconf")};
bool include_watchonly =
ParseIncludeWatchonly(request.params[2], *pwallet);
bool avoid_reuse = GetAvoidReuseFlag(pwallet, request.params[3]);
const auto bal = GetBalance(*pwallet, min_depth, avoid_reuse);
return bal.m_mine_trusted + (include_watchonly
? bal.m_watchonly_trusted
: Amount::zero());
},
};
}
static RPCHelpMan getunconfirmedbalance() {
return RPCHelpMan{
"getunconfirmedbalance",
"DEPRECATED\nIdentical to getbalances().mine.untrusted_pending\n",
{},
RPCResult{RPCResult::Type::NUM, "", "The balance"},
RPCExamples{""},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
return GetBalance(*pwallet).m_mine_untrusted_pending;
},
};
}
static RPCHelpMan sendmany() {
return RPCHelpMan{
"sendmany",
"Send multiple times. Amounts are double-precision "
"floating point numbers." +
HELP_REQUIRING_PASSPHRASE,
{
{"dummy", RPCArg::Type::STR, RPCArg::Optional::NO,
"Must be set to \"\" for backwards compatibility.",
RPCArgOptions{.skip_type_check = true,
.oneline_description = "\"\""}},
{
"amounts",
RPCArg::Type::OBJ_USER_KEYS,
RPCArg::Optional::NO,
"The addresses and amounts",
{
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO,
"The bitcoin address is the key, the numeric amount (can "
"be string) in " +
Currency::get().ticker + " is the value"},
},
},
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1},
"Only use the balance confirmed at least this many times."},
{"comment", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"A comment"},
{
"subtractfeefrom",
RPCArg::Type::ARR,
RPCArg::Optional::OMITTED,
"The addresses.\n"
" The fee will be equally deducted "
"from the amount of each selected address.\n"
" Those recipients will receive less "
"bitcoins than you enter in their corresponding amount field.\n"
" If no addresses are specified "
"here, the sender pays the fee.",
{
{"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"Subtract fee from this address"},
},
},
},
RPCResult{RPCResult::Type::STR_HEX, "txid",
"The transaction id for the send. Only 1 transaction is "
"created regardless of the number of addresses."},
RPCExamples{
"\nSend two amounts to two different addresses:\n" +
HelpExampleCli(
"sendmany",
"\"\" "
"\"{\\\"bchtest:qplljx455cznj2yrtdhj0jcm7syxlzqnaqt0ku5kjl\\\":"
"0.01,"
"\\\"bchtest:qzmnuh8t24yrxq4mvjakt84r7j3f9tunlvm2p7qef9\\\":0."
"02}\"") +
"\nSend two amounts to two different addresses setting the "
"confirmation and comment:\n" +
HelpExampleCli(
"sendmany",
"\"\" "
"\"{\\\"bchtest:qplljx455cznj2yrtdhj0jcm7syxlzqnaqt0ku5kjl\\\":"
"0.01,"
"\\\"bchtest:qzmnuh8t24yrxq4mvjakt84r7j3f9tunlvm2p7qef9\\\":0."
"02}\" "
"6 \"testing\"") +
"\nSend two amounts to two different addresses, subtract fee "
"from amount:\n" +
HelpExampleCli(
"sendmany",
"\"\" "
"\"{\\\"bchtest:qplljx455cznj2yrtdhj0jcm7syxlzqnaqt0ku5kjl\\\":"
"0.01,"
"\\\"bchtest:qzmnuh8t24yrxq4mvjakt84r7j3f9tunlvm2p7qef9\\\":0."
"02}\" 1 \"\" "
"\"[\\\"bchtest:qplljx455cznj2yrtdhj0jcm7syxlzqnaqt0ku5kjl\\\","
"\\\"bchtest:qzmnuh8t24yrxq4mvjakt84r7j3f9tunlvm2p7qef9\\\"]"
"\"") +
"\nAs a JSON-RPC call\n" +
HelpExampleRpc(
"sendmany",
"\"\", "
"{\"bchtest:qplljx455cznj2yrtdhj0jcm7syxlzqnaqt0ku5kjl\":0.01,"
"\"bchtest:qzmnuh8t24yrxq4mvjakt84r7j3f9tunlvm2p7qef9\":0.02}, "
"6, "
"\"testing\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
if (!request.params[0].isNull() &&
!request.params[0].get_str().empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Dummy value must be set to \"\"");
}
UniValue sendTo = request.params[1].get_obj();
mapValue_t mapValue;
if (!request.params[3].isNull() &&
!request.params[3].get_str().empty()) {
mapValue["comment"] = request.params[3].get_str();
}
UniValue subtractFeeFromAmount(UniValue::VARR);
if (!request.params[4].isNull()) {
subtractFeeFromAmount = request.params[4].get_array();
}
std::vector<CRecipient> recipients;
ParseRecipients(sendTo, subtractFeeFromAmount, recipients,
wallet->GetChainParams());
CCoinControl coin_control;
return SendMoney(pwallet, coin_control, recipients,
std::move(mapValue));
},
};
}
static RPCHelpMan addmultisigaddress() {
return RPCHelpMan{
"addmultisigaddress",
"Add an nrequired-to-sign multisignature address to the wallet. "
"Requires a new wallet backup.\n"
"Each key is a Bitcoin address or hex-encoded public key.\n"
"This functionality is only intended for use with non-watchonly "
"addresses.\n"
"See `importaddress` for watchonly p2sh address support.\n"
"If 'label' is specified (DEPRECATED), assign address to that label.\n"
"Note: This command is only compatible with legacy wallets.\n",
{
{"nrequired", RPCArg::Type::NUM, RPCArg::Optional::NO,
"The number of required signatures out of the n keys or "
"addresses."},
{
"keys",
RPCArg::Type::ARR,
RPCArg::Optional::NO,
"The bitcoin addresses or hex-encoded public keys",
{
{"key", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"bitcoin address or hex-encoded public key"},
},
},
{"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"A label to assign the addresses to."},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "address",
"The value of the new multisig address"},
{RPCResult::Type::STR_HEX, "redeemScript",
"The string value of the hex-encoded redemption script"},
{RPCResult::Type::STR, "descriptor",
"The descriptor for this multisig"},
}},
RPCExamples{
"\nAdd a multisig address from 2 addresses\n" +
HelpExampleCli("addmultisigaddress",
"2 "
"\"[\\\"16sSauSf5pF2UkUwvKGq4qjNRzBZYqgEL5\\\","
"\\\"171sgjn4YtPu27adkKGrdDwzRTxnRkBfKV\\\"]\"") +
"\nAs a JSON-RPC call\n" +
HelpExampleRpc("addmultisigaddress",
"2, "
"\"[\\\"16sSauSf5pF2UkUwvKGq4qjNRzBZYqgEL5\\\","
"\\\"171sgjn4YtPu27adkKGrdDwzRTxnRkBfKV\\\"]\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
LegacyScriptPubKeyMan &spk_man =
EnsureLegacyScriptPubKeyMan(*pwallet);
LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore);
std::string label;
if (!request.params[2].isNull()) {
label = LabelFromValue(request.params[2]);
}
int required = request.params[0].getInt<int>();
// Get the public keys
const UniValue &keys_or_addrs = request.params[1].get_array();
std::vector<CPubKey> pubkeys;
for (size_t i = 0; i < keys_or_addrs.size(); ++i) {
if (IsHex(keys_or_addrs[i].get_str()) &&
(keys_or_addrs[i].get_str().length() == 66 ||
keys_or_addrs[i].get_str().length() == 130)) {
pubkeys.push_back(HexToPubKey(keys_or_addrs[i].get_str()));
} else {
pubkeys.push_back(AddrToPubKey(wallet->GetChainParams(),
spk_man,
keys_or_addrs[i].get_str()));
}
}
OutputType output_type = pwallet->m_default_address_type;
// Construct using pay-to-script-hash:
CScript inner;
CTxDestination dest = AddAndGetMultisigDestination(
required, pubkeys, output_type, spk_man, inner);
pwallet->SetAddressBook(dest, label, "send");
// Make the descriptor
std::unique_ptr<Descriptor> descriptor =
InferDescriptor(GetScriptForDestination(dest), spk_man);
UniValue result(UniValue::VOBJ);
result.pushKV("address", EncodeDestination(dest, config));
result.pushKV("redeemScript", HexStr(inner));
result.pushKV("descriptor", descriptor->ToString());
return result;
},
};
}
struct tallyitem {
Amount nAmount{Amount::zero()};
int nConf{std::numeric_limits<int>::max()};
std::vector<uint256> txids;
bool fIsWatchonly{false};
tallyitem() {}
};
static UniValue ListReceived(const Config &config, const CWallet *const pwallet,
const UniValue &params, bool by_label)
EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) {
// Minimum confirmations
int nMinDepth = 1;
if (!params[0].isNull()) {
nMinDepth = params[0].getInt<int>();
}
// Whether to include empty labels
bool fIncludeEmpty = false;
if (!params[1].isNull()) {
fIncludeEmpty = params[1].get_bool();
}
isminefilter filter = ISMINE_SPENDABLE;
if (ParseIncludeWatchonly(params[2], *pwallet)) {
filter |= ISMINE_WATCH_ONLY;
}
bool has_filtered_address = false;
CTxDestination filtered_address = CNoDestination();
if (!by_label && params.size() > 3) {
if (!IsValidDestinationString(params[3].get_str(),
pwallet->GetChainParams())) {
throw JSONRPCError(RPC_WALLET_ERROR,
"address_filter parameter was invalid");
}
filtered_address =
DecodeDestination(params[3].get_str(), pwallet->GetChainParams());
has_filtered_address = true;
}
// Tally
std::map<CTxDestination, tallyitem> mapTally;
for (const std::pair<const TxId, CWalletTx> &pairWtx : pwallet->mapWallet) {
const CWalletTx &wtx = pairWtx.second;
if (wtx.IsCoinBase()) {
continue;
}
int nDepth = pwallet->GetTxDepthInMainChain(wtx);
if (nDepth < nMinDepth) {
continue;
}
for (const CTxOut &txout : wtx.tx->vout) {
CTxDestination address;
if (!ExtractDestination(txout.scriptPubKey, address)) {
continue;
}
if (has_filtered_address && !(filtered_address == address)) {
continue;
}
isminefilter mine = pwallet->IsMine(address);
if (!(mine & filter)) {
continue;
}
tallyitem &item = mapTally[address];
item.nAmount += txout.nValue;
item.nConf = std::min(item.nConf, nDepth);
item.txids.push_back(wtx.GetId());
if (mine & ISMINE_WATCH_ONLY) {
item.fIsWatchonly = true;
}
}
}
// Reply
UniValue ret(UniValue::VARR);
std::map<std::string, tallyitem> label_tally;
// Create m_address_book iterator
// If we aren't filtering, go from begin() to end()
auto start = pwallet->m_address_book.begin();
auto end = pwallet->m_address_book.end();
// If we are filtering, find() the applicable entry
if (has_filtered_address) {
start = pwallet->m_address_book.find(filtered_address);
if (start != end) {
end = std::next(start);
}
}
for (auto item_it = start; item_it != end; ++item_it) {
if (item_it->second.IsChange()) {
continue;
}
const CTxDestination &address = item_it->first;
const std::string &label = item_it->second.GetLabel();
std::map<CTxDestination, tallyitem>::iterator it =
mapTally.find(address);
if (it == mapTally.end() && !fIncludeEmpty) {
continue;
}
Amount nAmount = Amount::zero();
int nConf = std::numeric_limits<int>::max();
bool fIsWatchonly = false;
if (it != mapTally.end()) {
nAmount = (*it).second.nAmount;
nConf = (*it).second.nConf;
fIsWatchonly = (*it).second.fIsWatchonly;
}
if (by_label) {
tallyitem &_item = label_tally[label];
_item.nAmount += nAmount;
_item.nConf = std::min(_item.nConf, nConf);
_item.fIsWatchonly = fIsWatchonly;
} else {
UniValue obj(UniValue::VOBJ);
if (fIsWatchonly) {
obj.pushKV("involvesWatchonly", true);
}
obj.pushKV("address", EncodeDestination(address, config));
obj.pushKV("amount", nAmount);
obj.pushKV("confirmations",
(nConf == std::numeric_limits<int>::max() ? 0 : nConf));
obj.pushKV("label", label);
UniValue transactions(UniValue::VARR);
if (it != mapTally.end()) {
for (const uint256 &_item : (*it).second.txids) {
transactions.push_back(_item.GetHex());
}
}
obj.pushKV("txids", transactions);
ret.push_back(obj);
}
}
if (by_label) {
for (const auto &entry : label_tally) {
Amount nAmount = entry.second.nAmount;
int nConf = entry.second.nConf;
UniValue obj(UniValue::VOBJ);
if (entry.second.fIsWatchonly) {
obj.pushKV("involvesWatchonly", true);
}
obj.pushKV("amount", nAmount);
obj.pushKV("confirmations",
(nConf == std::numeric_limits<int>::max() ? 0 : nConf));
obj.pushKV("label", entry.first);
ret.push_back(obj);
}
}
return ret;
}
static RPCHelpMan listreceivedbyaddress() {
return RPCHelpMan{
"listreceivedbyaddress",
"List balances by receiving address.\n",
{
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1},
"The minimum number of confirmations before payments are "
"included."},
{"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false},
"Whether to include addresses that haven't received any "
"payments."},
{"include_watchonly", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Whether to include watch-only addresses (see 'importaddress')."},
{"address_filter", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"If present, only return information on this address."},
},
RPCResult{
RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::BOOL, "involvesWatchonly",
"Only returns true if imported addresses were involved "
"in transaction"},
{RPCResult::Type::STR, "address", "The receiving address"},
{RPCResult::Type::STR_AMOUNT, "amount",
"The total amount in " + Currency::get().ticker +
" received by the address"},
{RPCResult::Type::NUM, "confirmations",
"The number of confirmations of the most recent "
"transaction included"},
{RPCResult::Type::STR, "label",
"The label of the receiving address. The default label "
"is \"\""},
{RPCResult::Type::ARR,
"txids",
"",
{
{RPCResult::Type::STR_HEX, "txid",
"The ids of transactions received with the address"},
}},
}},
}},
RPCExamples{
HelpExampleCli("listreceivedbyaddress", "") +
HelpExampleCli("listreceivedbyaddress", "6 true") +
HelpExampleRpc("listreceivedbyaddress", "6, true, true") +
HelpExampleRpc(
"listreceivedbyaddress",
"6, true, true, \"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
return ListReceived(config, pwallet, request.params, false);
},
};
}
static RPCHelpMan listreceivedbylabel() {
return RPCHelpMan{
"listreceivedbylabel",
"List received transactions by label.\n",
{
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1},
"The minimum number of confirmations before payments are "
"included."},
{"include_empty", RPCArg::Type::BOOL, RPCArg::Default{false},
"Whether to include labels that haven't received any payments."},
{"include_watchonly", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Whether to include watch-only addresses (see 'importaddress')."},
},
RPCResult{
RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::BOOL, "involvesWatchonly",
"Only returns true if imported addresses were involved "
"in transaction"},
{RPCResult::Type::STR_AMOUNT, "amount",
"The total amount received by addresses with this label"},
{RPCResult::Type::NUM, "confirmations",
"The number of confirmations of the most recent "
"transaction included"},
{RPCResult::Type::STR, "label",
"The label of the receiving address. The default label "
"is \"\""},
}},
}},
RPCExamples{HelpExampleCli("listreceivedbylabel", "") +
HelpExampleCli("listreceivedbylabel", "6 true") +
HelpExampleRpc("listreceivedbylabel", "6, true, true")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
return ListReceived(config, pwallet, request.params, true);
},
};
}
static void MaybePushAddress(UniValue &entry, const CTxDestination &dest) {
if (IsValidDestination(dest)) {
entry.pushKV("address", EncodeDestination(dest, GetConfig()));
}
}
/**
* List transactions based on the given criteria.
*
* @param pwallet The wallet.
* @param wtx The wallet transaction.
* @param nMinDepth The minimum confirmation depth.
* @param fLong Whether to include the JSON version of the
* transaction.
* @param ret The vector into which the result is stored.
* @param filter_ismine The "is mine" filter flags.
* @param filter_label Optional label string to filter incoming transactions.
*/
template <class Vec>
static void ListTransactions(const CWallet *const pwallet, const CWalletTx &wtx,
int nMinDepth, bool fLong, Vec &ret,
const isminefilter &filter_ismine,
const std::string *filter_label)
EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet) {
Amount nFee;
std::list<COutputEntry> listReceived;
std::list<COutputEntry> listSent;
CachedTxGetAmounts(*pwallet, wtx, listReceived, listSent, nFee,
filter_ismine);
bool involvesWatchonly = CachedTxIsFromMe(*pwallet, wtx, ISMINE_WATCH_ONLY);
// Sent
if (!filter_label) {
for (const COutputEntry &s : listSent) {
UniValue entry(UniValue::VOBJ);
if (involvesWatchonly ||
(pwallet->IsMine(s.destination) & ISMINE_WATCH_ONLY)) {
entry.pushKV("involvesWatchonly", true);
}
MaybePushAddress(entry, s.destination);
entry.pushKV("category", "send");
entry.pushKV("amount", -s.amount);
const auto *address_book_entry =
pwallet->FindAddressBookEntry(s.destination);
if (address_book_entry) {
entry.pushKV("label", address_book_entry->GetLabel());
}
entry.pushKV("vout", s.vout);
entry.pushKV("fee", -1 * nFee);
if (fLong) {
WalletTxToJSON(*pwallet, wtx, entry);
}
entry.pushKV("abandoned", wtx.isAbandoned());
ret.push_back(entry);
}
}
// Received
if (listReceived.size() > 0 &&
pwallet->GetTxDepthInMainChain(wtx) >= nMinDepth) {
for (const COutputEntry &r : listReceived) {
std::string label;
const auto *address_book_entry =
pwallet->FindAddressBookEntry(r.destination);
if (address_book_entry) {
label = address_book_entry->GetLabel();
}
if (filter_label && label != *filter_label) {
continue;
}
UniValue entry(UniValue::VOBJ);
if (involvesWatchonly ||
(pwallet->IsMine(r.destination) & ISMINE_WATCH_ONLY)) {
entry.pushKV("involvesWatchonly", true);
}
MaybePushAddress(entry, r.destination);
if (wtx.IsCoinBase()) {
if (pwallet->GetTxDepthInMainChain(wtx) < 1) {
entry.pushKV("category", "orphan");
} else if (pwallet->IsTxImmatureCoinBase(wtx)) {
entry.pushKV("category", "immature");
} else {
entry.pushKV("category", "generate");
}
} else {
entry.pushKV("category", "receive");
}
entry.pushKV("amount", r.amount);
if (address_book_entry) {
entry.pushKV("label", label);
}
entry.pushKV("vout", r.vout);
if (fLong) {
WalletTxToJSON(*pwallet, wtx, entry);
}
ret.push_back(entry);
}
}
}
static const std::vector<RPCResult> TransactionDescriptionString() {
return {
{RPCResult::Type::NUM, "confirmations",
"The number of confirmations for the transaction. Negative "
"confirmations means the\n"
"transaction conflicted that many blocks ago."},
{RPCResult::Type::BOOL, "generated",
"Only present if transaction only input is a coinbase one."},
{RPCResult::Type::BOOL, "trusted",
"Only present if we consider transaction to be trusted and so safe to "
"spend from."},
{RPCResult::Type::STR_HEX, "blockhash",
"The block hash containing the transaction."},
{RPCResult::Type::NUM, "blockheight",
"The block height containing the transaction."},
{RPCResult::Type::NUM, "blockindex",
"The index of the transaction in the block that includes it."},
{RPCResult::Type::NUM_TIME, "blocktime",
"The block time expressed in " + UNIX_EPOCH_TIME + "."},
{RPCResult::Type::STR_HEX, "txid", "The transaction id."},
{RPCResult::Type::ARR,
"walletconflicts",
"Conflicting transaction ids.",
{
{RPCResult::Type::STR_HEX, "txid", "The transaction id."},
}},
{RPCResult::Type::NUM_TIME, "time",
"The transaction time expressed in " + UNIX_EPOCH_TIME + "."},
{RPCResult::Type::NUM_TIME, "timereceived",
"The time received expressed in " + UNIX_EPOCH_TIME + "."},
{RPCResult::Type::STR, "comment",
"If a comment is associated with the transaction, only present if not "
"empty."},
};
}
RPCHelpMan listtransactions() {
const auto &ticker = Currency::get().ticker;
return RPCHelpMan{
"listtransactions",
"If a label name is provided, this will return only incoming "
"transactions paying to addresses with the specified label.\n"
"\nReturns up to 'count' most recent transactions skipping the first "
"'from' transactions.\n",
{
{"label|dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"If set, should be a valid label name to return only incoming "
"transactions with the specified label, or \"*\" to disable "
"filtering and return all transactions."},
{"count", RPCArg::Type::NUM, RPCArg::Default{10},
"The number of transactions to return"},
{"skip", RPCArg::Type::NUM, RPCArg::Default{0},
"The number of transactions to skip"},
{"include_watchonly", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Include transactions to watch-only addresses (see "
"'importaddress')"},
},
RPCResult{
RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::OBJ, "", "",
Cat(Cat<std::vector<RPCResult>>(
{
{RPCResult::Type::BOOL, "involvesWatchonly",
"Only returns true if imported addresses were "
"involved in transaction."},
{RPCResult::Type::STR, "address",
"The bitcoin address of the transaction."},
{RPCResult::Type::STR, "category",
"The transaction category.\n"
"\"send\" Transactions sent.\n"
"\"receive\" Non-coinbase "
"transactions received.\n"
"\"generate\" Coinbase transactions "
"received with more than 100 confirmations.\n"
"\"immature\" Coinbase transactions "
"received with 100 or fewer confirmations.\n"
"\"orphan\" Orphaned coinbase "
"transactions received."},
{RPCResult::Type::STR_AMOUNT, "amount",
"The amount in " + ticker +
". This is negative for the 'send' category, "
"and is positive\n"
"for all other categories"},
{RPCResult::Type::STR, "label",
"A comment for the address/transaction, if any"},
{RPCResult::Type::NUM, "vout", "the vout value"},
{RPCResult::Type::STR_AMOUNT, "fee",
"The amount of the fee in " + ticker +
". This is negative and only available for "
"the\n"
"'send' category of transactions."},
},
TransactionDescriptionString()),
{
{RPCResult::Type::BOOL, "abandoned",
"'true' if the transaction has been abandoned "
"(inputs are respendable). Only available for the \n"
"'send' category of transactions."},
})},
}},
RPCExamples{"\nList the most recent 10 transactions in the systems\n" +
HelpExampleCli("listtransactions", "") +
"\nList transactions 100 to 120\n" +
HelpExampleCli("listtransactions", "\"*\" 20 100") +
"\nAs a JSON-RPC call\n" +
HelpExampleRpc("listtransactions", "\"*\", 20, 100")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
const std::string *filter_label = nullptr;
if (!request.params[0].isNull() &&
request.params[0].get_str() != "*") {
filter_label = &request.params[0].get_str();
if (filter_label->empty()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Label argument must be a valid label name or \"*\".");
}
}
int nCount = 10;
if (!request.params[1].isNull()) {
nCount = request.params[1].getInt<int>();
}
int nFrom = 0;
if (!request.params[2].isNull()) {
nFrom = request.params[2].getInt<int>();
}
isminefilter filter = ISMINE_SPENDABLE;
if (ParseIncludeWatchonly(request.params[3], *pwallet)) {
filter |= ISMINE_WATCH_ONLY;
}
if (nCount < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative count");
}
if (nFrom < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative from");
}
std::vector<UniValue> ret;
{
LOCK(pwallet->cs_wallet);
const CWallet::TxItems &txOrdered = pwallet->wtxOrdered;
// iterate backwards until we have nCount items to return:
for (CWallet::TxItems::const_reverse_iterator it =
txOrdered.rbegin();
it != txOrdered.rend(); ++it) {
CWalletTx *const pwtx = (*it).second;
ListTransactions(pwallet, *pwtx, 0, true, ret, filter,
filter_label);
if (int(ret.size()) >= (nCount + nFrom)) {
break;
}
}
}
// ret is newest to oldest
if (nFrom > (int)ret.size()) {
nFrom = ret.size();
}
if ((nFrom + nCount) > (int)ret.size()) {
nCount = ret.size() - nFrom;
}
auto txs_rev_it{std::make_move_iterator(ret.rend())};
UniValue result{UniValue::VARR};
// Return oldest to newest
result.push_backV(txs_rev_it - nFrom - nCount, txs_rev_it - nFrom);
return result;
},
};
}
static RPCHelpMan listsinceblock() {
const auto &ticker = Currency::get().ticker;
return RPCHelpMan{
"listsinceblock",
"Get all transactions in blocks since block [blockhash], or all "
"transactions if omitted.\n"
"If \"blockhash\" is no longer a part of the main chain, transactions "
"from the fork point onward are included.\n"
"Additionally, if include_removed is set, transactions affecting the "
"wallet which were removed are returned in the \"removed\" array.\n",
{
{"blockhash", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"If set, the block hash to list transactions since, otherwise "
"list all transactions."},
{"target_confirmations", RPCArg::Type::NUM, RPCArg::Default{1},
"Return the nth block hash from the main chain. e.g. 1 would mean "
"the best block hash. Note: this is not used as a filter, but "
"only affects [lastblock] in the return value"},
{"include_watchonly", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Include transactions to watch-only addresses (see "
"'importaddress')"},
{"include_removed", RPCArg::Type::BOOL, RPCArg::Default{true},
"Show transactions that were removed due to a reorg in the "
"\"removed\" array\n"
" (not "
"guaranteed to work on pruned nodes)"},
},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::ARR,
"transactions",
"",
{
{RPCResult::Type::OBJ, "", "",
Cat(Cat<std::vector<RPCResult>>(
{
{RPCResult::Type::BOOL, "involvesWatchonly",
"Only returns true if imported addresses "
"were involved in transaction."},
{RPCResult::Type::STR, "address",
"The bitcoin address of the transaction."},
{RPCResult::Type::STR, "category",
"The transaction category.\n"
"\"send\" Transactions "
"sent.\n"
"\"receive\" Non-coinbase "
"transactions received.\n"
"\"generate\" Coinbase "
"transactions received with more than 100 "
"confirmations.\n"
"\"immature\" Coinbase "
"transactions received with 100 or fewer "
"confirmations.\n"
"\"orphan\" Orphaned "
"coinbase transactions received."},
{RPCResult::Type::STR_AMOUNT, "amount",
"The amount in " + ticker +
". This is negative for the 'send' "
"category, and is positive\n"
"for all other categories"},
{RPCResult::Type::NUM, "vout",
"the vout value"},
{RPCResult::Type::STR_AMOUNT, "fee",
"The amount of the fee in " + ticker +
". This is negative and only available "
"for the\n"
"'send' category of transactions."},
},
TransactionDescriptionString()),
{
{RPCResult::Type::BOOL, "abandoned",
"'true' if the transaction has been abandoned "
"(inputs are respendable). Only available for "
"the \n"
"'send' category of transactions."},
{RPCResult::Type::STR, "comment",
"If a comment is associated with the "
"transaction."},
{RPCResult::Type::STR, "label",
"A comment for the address/transaction, if any"},
{RPCResult::Type::STR, "to",
"If a comment to is associated with the "
"transaction."},
})},
}},
{RPCResult::Type::ARR,
"removed",
"<structure is the same as \"transactions\" above, only "
"present if include_removed=true>\n"
"Note: transactions that were re-added in the active chain "
"will appear as-is in this array, and may thus have a "
"positive confirmation count.",
{
{RPCResult::Type::ELISION, "", ""},
}},
{RPCResult::Type::STR_HEX, "lastblock",
"The hash of the block (target_confirmations-1) from the best "
"block on the main chain, or the genesis hash if the "
"referenced block does not exist yet. This is typically used "
"to feed back into listsinceblock the next time you call it. "
"So you would generally use a target_confirmations of say 6, "
"so you will be continually re-notified of transactions until "
"they've reached 6 confirmations plus any new ones"},
}},
RPCExamples{HelpExampleCli("listsinceblock", "") +
HelpExampleCli("listsinceblock",
"\"000000000000000bacf66f7497b7dc45ef753ee9a"
"7d38571037cdb1a57f663ad\" 6") +
HelpExampleRpc("listsinceblock",
"\"000000000000000bacf66f7497b7dc45ef753ee9a"
"7d38571037cdb1a57f663ad\", 6")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const pwallet =
GetWalletForJSONRPCRequest(request);
if (!pwallet) {
return NullUniValue;
}
const CWallet &wallet = *pwallet;
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
wallet.BlockUntilSyncedToCurrentChain();
LOCK(wallet.cs_wallet);
// Height of the specified block or the common ancestor, if the
// block provided was in a deactivated chain.
std::optional<int> height;
// Height of the specified block, even if it's in a deactivated
// chain.
std::optional<int> altheight;
int target_confirms = 1;
isminefilter filter = ISMINE_SPENDABLE;
BlockHash blockId;
if (!request.params[0].isNull() &&
!request.params[0].get_str().empty()) {
blockId = BlockHash(ParseHashV(request.params[0], "blockhash"));
height = int{};
altheight = int{};
if (!wallet.chain().findCommonAncestor(
blockId, wallet.GetLastBlockHash(),
/* ancestor out */ FoundBlock().height(*height),
/* blockId out */ FoundBlock().height(*altheight))) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Block not found");
}
}
if (!request.params[1].isNull()) {
target_confirms = request.params[1].getInt<int>();
if (target_confirms < 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Invalid parameter");
}
}
if (ParseIncludeWatchonly(request.params[2], wallet)) {
filter |= ISMINE_WATCH_ONLY;
}
bool include_removed =
(request.params[3].isNull() || request.params[3].get_bool());
int depth = height ? wallet.GetLastBlockHeight() + 1 - *height : -1;
UniValue transactions(UniValue::VARR);
for (const std::pair<const TxId, CWalletTx> &pairWtx :
wallet.mapWallet) {
const CWalletTx &tx = pairWtx.second;
if (depth == -1 || wallet.GetTxDepthInMainChain(tx) < depth) {
ListTransactions(&wallet, tx, 0, true, transactions, filter,
nullptr /* filter_label */);
}
}
// when a reorg'd block is requested, we also list any relevant
// transactions in the blocks of the chain that was detached
UniValue removed(UniValue::VARR);
while (include_removed && altheight && *altheight > *height) {
CBlock block;
if (!wallet.chain().findBlock(blockId,
FoundBlock().data(block)) ||
block.IsNull()) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Can't read block from disk");
}
for (const CTransactionRef &tx : block.vtx) {
auto it = wallet.mapWallet.find(tx->GetId());
if (it != wallet.mapWallet.end()) {
// We want all transactions regardless of confirmation
// count to appear here, even negative confirmation
// ones, hence the big negative.
ListTransactions(&wallet, it->second, -100000000, true,
removed, filter,
nullptr /* filter_label */);
}
}
blockId = block.hashPrevBlock;
--*altheight;
}
BlockHash lastblock;
target_confirms =
std::min(target_confirms, wallet.GetLastBlockHeight() + 1);
CHECK_NONFATAL(wallet.chain().findAncestorByHeight(
wallet.GetLastBlockHash(),
wallet.GetLastBlockHeight() + 1 - target_confirms,
FoundBlock().hash(lastblock)));
UniValue ret(UniValue::VOBJ);
ret.pushKV("transactions", transactions);
if (include_removed) {
ret.pushKV("removed", removed);
}
ret.pushKV("lastblock", lastblock.GetHex());
return ret;
},
};
}
static RPCHelpMan gettransaction() {
const auto &ticker = Currency::get().ticker;
return RPCHelpMan{
"gettransaction",
"Get detailed information about in-wallet transaction <txid>\n",
{
{"txid", RPCArg::Type::STR, RPCArg::Optional::NO,
"The transaction id"},
{"include_watchonly", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Whether to include watch-only addresses in balance calculation "
"and details[]"},
{"verbose", RPCArg::Type::BOOL, RPCArg::Default{false},
"Whether to include a `decoded` field containing the decoded "
"transaction (equivalent to RPC decoderawtransaction)"},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
Cat(Cat<std::vector<RPCResult>>(
{
{RPCResult::Type::STR_AMOUNT, "amount",
"The amount in " + ticker},
{RPCResult::Type::STR_AMOUNT, "fee",
"The amount of the fee in " + ticker +
". This is negative and only available for the\n"
"'send' category of transactions."},
},
TransactionDescriptionString()),
{
{RPCResult::Type::ARR,
"details",
"",
{
{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::BOOL, "involvesWatchonly",
"Only returns true if imported addresses were "
"involved in transaction."},
{RPCResult::Type::STR, "address",
"The bitcoin address involved in the "
"transaction."},
{RPCResult::Type::STR, "category",
"The transaction category.\n"
"\"send\" Transactions sent.\n"
"\"receive\" Non-coinbase "
"transactions received.\n"
"\"generate\" Coinbase "
"transactions received with more than 100 "
"confirmations.\n"
"\"immature\" Coinbase "
"transactions received with 100 or fewer "
"confirmations.\n"
"\"orphan\" Orphaned coinbase "
"transactions received."},
{RPCResult::Type::STR_AMOUNT, "amount",
"The amount in " + ticker},
{RPCResult::Type::STR, "label",
"A comment for the address/transaction, if any"},
{RPCResult::Type::NUM, "vout", "the vout value"},
{RPCResult::Type::STR_AMOUNT, "fee",
"The amount of the fee in " + ticker +
". This is negative and only available for "
"the \n"
"'send' category of transactions."},
{RPCResult::Type::BOOL, "abandoned",
"'true' if the transaction has been abandoned "
"(inputs are respendable). Only available for "
"the \n"
"'send' category of transactions."},
}},
}},
{RPCResult::Type::STR_HEX, "hex",
"Raw data for transaction"},
{RPCResult::Type::OBJ,
"decoded",
"Optional, the decoded transaction (only present when "
"`verbose` is passed)",
{
{RPCResult::Type::ELISION, "",
"Equivalent to the RPC decoderawtransaction method, "
"or the RPC getrawtransaction method when `verbose` "
"is passed."},
}},
})},
RPCExamples{HelpExampleCli("gettransaction",
"\"1075db55d416d3ca199f55b6084e2115b9345e16c"
"5cf302fc80e9d5fbf5d48d\"") +
HelpExampleCli("gettransaction",
"\"1075db55d416d3ca199f55b6084e2115b9345e16c"
"5cf302fc80e9d5fbf5d48d\" true") +
HelpExampleCli("gettransaction",
"\"1075db55d416d3ca199f55b6084e2115b9345e16c"
"5cf302fc80e9d5fbf5d48d\" false true") +
HelpExampleRpc("gettransaction",
"\"1075db55d416d3ca199f55b6084e2115b9345e16c"
"5cf302fc80e9d5fbf5d48d\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
TxId txid(ParseHashV(request.params[0], "txid"));
isminefilter filter = ISMINE_SPENDABLE;
if (ParseIncludeWatchonly(request.params[1], *pwallet)) {
filter |= ISMINE_WATCH_ONLY;
}
bool verbose = request.params[2].isNull()
? false
: request.params[2].get_bool();
UniValue entry(UniValue::VOBJ);
auto it = pwallet->mapWallet.find(txid);
if (it == pwallet->mapWallet.end()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Invalid or non-wallet transaction id");
}
const CWalletTx &wtx = it->second;
Amount nCredit = CachedTxGetCredit(*pwallet, wtx, filter);
Amount nDebit = CachedTxGetDebit(*pwallet, wtx, filter);
Amount nNet = nCredit - nDebit;
Amount nFee = (CachedTxIsFromMe(*pwallet, wtx, filter)
? wtx.tx->GetValueOut() - nDebit
: Amount::zero());
entry.pushKV("amount", nNet - nFee);
if (CachedTxIsFromMe(*pwallet, wtx, filter)) {
entry.pushKV("fee", nFee);
}
WalletTxToJSON(*pwallet, wtx, entry);
UniValue details(UniValue::VARR);
ListTransactions(pwallet, wtx, 0, false, details, filter,
nullptr /* filter_label */);
entry.pushKV("details", details);
std::string strHex =
EncodeHexTx(*wtx.tx, pwallet->chain().rpcSerializationFlags());
entry.pushKV("hex", strHex);
if (verbose) {
UniValue decoded(UniValue::VOBJ);
TxToUniv(*wtx.tx, BlockHash(), decoded, false);
entry.pushKV("decoded", decoded);
}
return entry;
},
};
}
static RPCHelpMan abandontransaction() {
return RPCHelpMan{
"abandontransaction",
"Mark in-wallet transaction <txid> as abandoned\n"
"This will mark this transaction and all its in-wallet descendants as "
"abandoned which will allow\n"
"for their inputs to be respent. It can be used to replace \"stuck\" "
"or evicted transactions.\n"
"It only works on transactions which are not included in a block and "
"are not currently in the mempool.\n"
"It has no effect on transactions which are already abandoned.\n",
{
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
"The transaction id"},
},
RPCResult{RPCResult::Type::NONE, "", ""},
RPCExamples{HelpExampleCli("abandontransaction",
"\"1075db55d416d3ca199f55b6084e2115b9345e16c"
"5cf302fc80e9d5fbf5d48d\"") +
HelpExampleRpc("abandontransaction",
"\"1075db55d416d3ca199f55b6084e2115b9345e16c"
"5cf302fc80e9d5fbf5d48d\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
TxId txid(ParseHashV(request.params[0], "txid"));
if (!pwallet->mapWallet.count(txid)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Invalid or non-wallet transaction id");
}
if (!pwallet->AbandonTransaction(txid)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Transaction not eligible for abandonment");
}
return NullUniValue;
},
};
}
static RPCHelpMan keypoolrefill() {
return RPCHelpMan{
"keypoolrefill",
"Fills the keypool." + HELP_REQUIRING_PASSPHRASE,
{
{"newsize", RPCArg::Type::NUM, RPCArg::Default{100},
"The new keypool size"},
},
RPCResult{RPCResult::Type::NONE, "", ""},
RPCExamples{HelpExampleCli("keypoolrefill", "") +
HelpExampleRpc("keypoolrefill", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
if (pwallet->IsLegacy() &&
pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
throw JSONRPCError(
RPC_WALLET_ERROR,
"Error: Private keys are disabled for this wallet");
}
LOCK(pwallet->cs_wallet);
// 0 is interpreted by TopUpKeyPool() as the default keypool size
// given by -keypool
unsigned int kpSize = 0;
if (!request.params[0].isNull()) {
if (request.params[0].getInt<int>() < 0) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, expected valid size.");
}
kpSize = (unsigned int)request.params[0].getInt<int>();
}
EnsureWalletIsUnlocked(pwallet);
pwallet->TopUpKeyPool(kpSize);
if (pwallet->GetKeyPoolSize() < kpSize) {
throw JSONRPCError(RPC_WALLET_ERROR,
"Error refreshing keypool.");
}
return NullUniValue;
},
};
}
static RPCHelpMan lockunspent() {
return RPCHelpMan{
"lockunspent",
"Updates list of temporarily unspendable outputs.\n"
"Temporarily lock (unlock=false) or unlock (unlock=true) specified "
"transaction outputs.\n"
"If no transaction outputs are specified when unlocking then all "
"current locked transaction outputs are unlocked.\n"
"A locked transaction output will not be chosen by automatic coin "
"selection, when spending bitcoins.\n"
"Manually selected coins are automatically unlocked.\n"
"Locks are stored in memory only. Nodes start with zero locked "
"outputs, and the locked output list\n"
"is always cleared (by virtue of process exit) when a node stops or "
"fails.\n"
"Also see the listunspent call\n",
{
{"unlock", RPCArg::Type::BOOL, RPCArg::Optional::NO,
"Whether to unlock (true) or lock (false) the specified "
"transactions"},
{
"transactions",
RPCArg::Type::ARR,
RPCArg::Default{UniValue::VARR},
"The transaction outputs and within each, txid (string) vout "
"(numeric).",
{
{
"",
RPCArg::Type::OBJ,
RPCArg::Optional::OMITTED,
"",
{
{"txid", RPCArg::Type::STR_HEX,
RPCArg::Optional::NO, "The transaction id"},
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO,
"The output number"},
},
},
},
},
},
RPCResult{RPCResult::Type::BOOL, "",
"Whether the command was successful or not"},
RPCExamples{
"\nList the unspent transactions\n" +
HelpExampleCli("listunspent", "") +
"\nLock an unspent transaction\n" +
HelpExampleCli("lockunspent", "false "
"\"[{\\\"txid\\\":"
"\\\"a08e6907dbbd3d809776dbfc5d82e371"
"b764ed838b5655e72f463568df1aadf0\\\""
",\\\"vout\\\":1}]\"") +
"\nList the locked transactions\n" +
HelpExampleCli("listlockunspent", "") +
"\nUnlock the transaction again\n" +
HelpExampleCli("lockunspent", "true "
"\"[{\\\"txid\\\":"
"\\\"a08e6907dbbd3d809776dbfc5d82e371"
"b764ed838b5655e72f463568df1aadf0\\\""
",\\\"vout\\\":1}]\"") +
"\nAs a JSON-RPC call\n" +
HelpExampleRpc("lockunspent", "false, "
"\"[{\\\"txid\\\":"
"\\\"a08e6907dbbd3d809776dbfc5d82e371"
"b764ed838b5655e72f463568df1aadf0\\\""
",\\\"vout\\\":1}]\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
bool fUnlock = request.params[0].get_bool();
if (request.params[1].isNull()) {
if (fUnlock) {
pwallet->UnlockAllCoins();
}
return true;
}
const UniValue &output_params = request.params[1].get_array();
// Create and validate the COutPoints first.
std::vector<COutPoint> outputs;
outputs.reserve(output_params.size());
for (size_t idx = 0; idx < output_params.size(); idx++) {
const UniValue &o = output_params[idx].get_obj();
RPCTypeCheckObj(o, {
{"txid", UniValueType(UniValue::VSTR)},
{"vout", UniValueType(UniValue::VNUM)},
});
const int nOutput = o.find_value("vout").getInt<int>();
if (nOutput < 0) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, vout cannot be negative");
}
const TxId txid(ParseHashO(o, "txid"));
const auto it = pwallet->mapWallet.find(txid);
if (it == pwallet->mapWallet.end()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, unknown transaction");
}
const COutPoint output(txid, nOutput);
const CWalletTx &trans = it->second;
if (output.GetN() >= trans.tx->vout.size()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, vout index out of bounds");
}
if (pwallet->IsSpent(output)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, expected unspent output");
}
const bool is_locked = pwallet->IsLockedCoin(output);
if (fUnlock && !is_locked) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, expected locked output");
}
if (!fUnlock && is_locked) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Invalid parameter, output already locked");
}
outputs.push_back(output);
}
// Atomically set (un)locked status for the outputs.
for (const COutPoint &output : outputs) {
if (fUnlock) {
pwallet->UnlockCoin(output);
} else {
pwallet->LockCoin(output);
}
}
return true;
},
};
}
static RPCHelpMan listlockunspent() {
return RPCHelpMan{
"listlockunspent",
"Returns list of temporarily unspendable outputs.\n"
"See the lockunspent call to lock and unlock transactions for "
"spending.\n",
{},
RPCResult{RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR_HEX, "txid",
"The transaction id locked"},
{RPCResult::Type::NUM, "vout", "The vout value"},
}},
}},
RPCExamples{
"\nList the unspent transactions\n" +
HelpExampleCli("listunspent", "") +
"\nLock an unspent transaction\n" +
HelpExampleCli("lockunspent", "false "
"\"[{\\\"txid\\\":"
"\\\"a08e6907dbbd3d809776dbfc5d82e371"
"b764ed838b5655e72f463568df1aadf0\\\""
",\\\"vout\\\":1}]\"") +
"\nList the locked transactions\n" +
HelpExampleCli("listlockunspent", "") +
"\nUnlock the transaction again\n" +
HelpExampleCli("lockunspent", "true "
"\"[{\\\"txid\\\":"
"\\\"a08e6907dbbd3d809776dbfc5d82e371"
"b764ed838b5655e72f463568df1aadf0\\\""
",\\\"vout\\\":1}]\"") +
"\nAs a JSON-RPC call\n" + HelpExampleRpc("listlockunspent", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
std::vector<COutPoint> vOutpts;
pwallet->ListLockedCoins(vOutpts);
UniValue ret(UniValue::VARR);
for (const COutPoint &output : vOutpts) {
UniValue o(UniValue::VOBJ);
o.pushKV("txid", output.GetTxId().GetHex());
o.pushKV("vout", int(output.GetN()));
ret.push_back(o);
}
return ret;
},
};
}
static RPCHelpMan settxfee() {
return RPCHelpMan{
"settxfee",
"Set the transaction fee per kB for this wallet. Overrides the "
"global -paytxfee command line parameter.\n"
"Can be deactivated by passing 0 as the fee. In that case automatic "
"fee selection will be used by default.\n",
{
{"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO,
"The transaction fee in " + Currency::get().ticker + "/kB"},
},
RPCResult{RPCResult::Type::BOOL, "", "Returns true if successful"},
RPCExamples{HelpExampleCli("settxfee", "0.00001") +
HelpExampleRpc("settxfee", "0.00001")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
Amount nAmount = AmountFromValue(request.params[0]);
CFeeRate tx_fee_rate(nAmount, 1000);
CFeeRate max_tx_fee_rate(pwallet->m_default_max_tx_fee, 1000);
if (tx_fee_rate == CFeeRate()) {
// automatic selection
} else if (tx_fee_rate < pwallet->chain().relayMinFee()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("txfee cannot be less than min relay tx fee (%s)",
pwallet->chain().relayMinFee().ToString()));
} else if (tx_fee_rate < pwallet->m_min_fee) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("txfee cannot be less than wallet min fee (%s)",
pwallet->m_min_fee.ToString()));
} else if (tx_fee_rate > max_tx_fee_rate) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf(
"txfee cannot be more than wallet max tx fee (%s)",
max_tx_fee_rate.ToString()));
}
pwallet->m_pay_tx_fee = tx_fee_rate;
return true;
},
};
}
static RPCHelpMan getbalances() {
return RPCHelpMan{
"getbalances",
"Returns an object with all balances in " + Currency::get().ticker +
".\n",
{},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::OBJ,
"mine",
"balances from outputs that the wallet can sign",
{
{RPCResult::Type::STR_AMOUNT, "trusted",
"trusted balance (outputs created by the wallet or "
"confirmed outputs)"},
{RPCResult::Type::STR_AMOUNT, "untrusted_pending",
"untrusted pending balance (outputs created by "
"others that are in the mempool)"},
{RPCResult::Type::STR_AMOUNT, "immature",
"balance from immature coinbase outputs"},
{RPCResult::Type::STR_AMOUNT, "used",
"(only present if avoid_reuse is set) balance from "
"coins sent to addresses that were previously "
"spent from (potentially privacy violating)"},
}},
{RPCResult::Type::OBJ,
"watchonly",
"watchonly balances (not present if wallet does not "
"watch anything)",
{
{RPCResult::Type::STR_AMOUNT, "trusted",
"trusted balance (outputs created by the wallet or "
"confirmed outputs)"},
{RPCResult::Type::STR_AMOUNT, "untrusted_pending",
"untrusted pending balance (outputs created by "
"others that are in the mempool)"},
{RPCResult::Type::STR_AMOUNT, "immature",
"balance from immature coinbase outputs"},
}},
}},
RPCExamples{HelpExampleCli("getbalances", "") +
HelpExampleRpc("getbalances", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const rpc_wallet =
GetWalletForJSONRPCRequest(request);
if (!rpc_wallet) {
return NullUniValue;
}
CWallet &wallet = *rpc_wallet;
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
wallet.BlockUntilSyncedToCurrentChain();
LOCK(wallet.cs_wallet);
const auto bal = GetBalance(wallet);
UniValue balances{UniValue::VOBJ};
{
UniValue balances_mine{UniValue::VOBJ};
balances_mine.pushKV("trusted", bal.m_mine_trusted);
balances_mine.pushKV("untrusted_pending",
bal.m_mine_untrusted_pending);
balances_mine.pushKV("immature", bal.m_mine_immature);
if (wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE)) {
// If the AVOID_REUSE flag is set, bal has been set to just
// the un-reused address balance. Get the total balance, and
// then subtract bal to get the reused address balance.
const auto full_bal = GetBalance(wallet, 0, false);
balances_mine.pushKV("used",
full_bal.m_mine_trusted +
full_bal.m_mine_untrusted_pending -
bal.m_mine_trusted -
bal.m_mine_untrusted_pending);
}
balances.pushKV("mine", balances_mine);
}
auto spk_man = wallet.GetLegacyScriptPubKeyMan();
if (spk_man && spk_man->HaveWatchOnly()) {
UniValue balances_watchonly{UniValue::VOBJ};
balances_watchonly.pushKV("trusted", bal.m_watchonly_trusted);
balances_watchonly.pushKV("untrusted_pending",
bal.m_watchonly_untrusted_pending);
balances_watchonly.pushKV("immature", bal.m_watchonly_immature);
balances.pushKV("watchonly", balances_watchonly);
}
return balances;
},
};
}
static RPCHelpMan getwalletinfo() {
return RPCHelpMan{
"getwalletinfo",
"Returns an object containing various wallet state info.\n",
{},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{{
{RPCResult::Type::STR, "walletname", "the wallet name"},
{RPCResult::Type::NUM, "walletversion", "the wallet version"},
{RPCResult::Type::STR_AMOUNT, "balance",
"DEPRECATED. Identical to getbalances().mine.trusted"},
{RPCResult::Type::STR_AMOUNT, "unconfirmed_balance",
"DEPRECATED. Identical to "
"getbalances().mine.untrusted_pending"},
{RPCResult::Type::STR_AMOUNT, "immature_balance",
"DEPRECATED. Identical to getbalances().mine.immature"},
{RPCResult::Type::NUM, "txcount",
"the total number of transactions in the wallet"},
{RPCResult::Type::NUM_TIME, "keypoololdest",
"the " + UNIX_EPOCH_TIME +
" of the oldest pre-generated key in the key pool. "
"Legacy wallets only."},
{RPCResult::Type::NUM, "keypoolsize",
"how many new keys are pre-generated (only counts external "
"keys)"},
{RPCResult::Type::NUM, "keypoolsize_hd_internal",
"how many new keys are pre-generated for internal use (used "
"for change outputs, only appears if the wallet is using "
"this feature, otherwise external keys are used)"},
{RPCResult::Type::NUM_TIME, "unlocked_until",
/* optional */ true,
"the " + UNIX_EPOCH_TIME +
" until which the wallet is unlocked for transfers, or 0 "
"if the wallet is locked (only present for "
"passphrase-encrypted wallets)"},
{RPCResult::Type::STR_AMOUNT, "paytxfee",
"the transaction fee configuration, set in " +
Currency::get().ticker + "/kB"},
{RPCResult::Type::STR_HEX, "hdseedid", /* optional */ true,
"the Hash160 of the HD seed (only present when HD is "
"enabled)"},
{RPCResult::Type::BOOL, "private_keys_enabled",
"false if privatekeys are disabled for this wallet (enforced "
"watch-only wallet)"},
{RPCResult::Type::OBJ,
"scanning",
"current scanning details, or false if no scan is in "
"progress",
{
{RPCResult::Type::NUM, "duration",
"elapsed seconds since scan start"},
{RPCResult::Type::NUM, "progress",
"scanning progress percentage [0.0, 1.0]"},
},
/*skip_type_check=*/true},
{RPCResult::Type::BOOL, "avoid_reuse",
"whether this wallet tracks clean/dirty coins in terms of "
"reuse"},
{RPCResult::Type::BOOL, "descriptors",
"whether this wallet uses descriptors for scriptPubKey "
"management"},
}},
},
RPCExamples{HelpExampleCli("getwalletinfo", "") +
HelpExampleRpc("getwalletinfo", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
UniValue obj(UniValue::VOBJ);
size_t kpExternalSize = pwallet->KeypoolCountExternalKeys();
const auto bal = GetBalance(*pwallet);
int64_t kp_oldest = pwallet->GetOldestKeyPoolTime();
obj.pushKV("walletname", pwallet->GetName());
obj.pushKV("walletversion", pwallet->GetVersion());
obj.pushKV("balance", bal.m_mine_trusted);
obj.pushKV("unconfirmed_balance", bal.m_mine_untrusted_pending);
obj.pushKV("immature_balance", bal.m_mine_immature);
obj.pushKV("txcount", (int)pwallet->mapWallet.size());
if (kp_oldest > 0) {
obj.pushKV("keypoololdest", kp_oldest);
}
obj.pushKV("keypoolsize", (int64_t)kpExternalSize);
LegacyScriptPubKeyMan *spk_man =
pwallet->GetLegacyScriptPubKeyMan();
if (spk_man) {
CKeyID seed_id = spk_man->GetHDChain().seed_id;
if (!seed_id.IsNull()) {
obj.pushKV("hdseedid", seed_id.GetHex());
}
}
if (pwallet->CanSupportFeature(FEATURE_HD_SPLIT)) {
obj.pushKV("keypoolsize_hd_internal",
int64_t(pwallet->GetKeyPoolSize() - kpExternalSize));
}
if (pwallet->IsCrypted()) {
obj.pushKV("unlocked_until", pwallet->nRelockTime);
}
obj.pushKV("paytxfee", pwallet->m_pay_tx_fee.GetFeePerK());
obj.pushKV(
"private_keys_enabled",
!pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
if (pwallet->IsScanning()) {
UniValue scanning(UniValue::VOBJ);
scanning.pushKV("duration", pwallet->ScanningDuration() / 1000);
scanning.pushKV("progress", pwallet->ScanningProgress());
obj.pushKV("scanning", scanning);
} else {
obj.pushKV("scanning", false);
}
obj.pushKV("avoid_reuse",
pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE));
obj.pushKV("descriptors",
pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
return obj;
},
};
}
static RPCHelpMan listwalletdir() {
return RPCHelpMan{
"listwalletdir",
"Returns a list of wallets in the wallet directory.\n",
{},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::ARR,
"wallets",
"",
{
{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "name", "The wallet name"},
}},
}},
}},
RPCExamples{HelpExampleCli("listwalletdir", "") +
HelpExampleRpc("listwalletdir", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
UniValue wallets(UniValue::VARR);
for (const auto &path : ListWalletDir()) {
UniValue wallet(UniValue::VOBJ);
wallet.pushKV("name", path.u8string());
wallets.push_back(wallet);
}
UniValue result(UniValue::VOBJ);
result.pushKV("wallets", wallets);
return result;
},
};
}
static RPCHelpMan listwallets() {
return RPCHelpMan{
"listwallets",
"Returns a list of currently loaded wallets.\n"
"For full information on the wallet, use \"getwalletinfo\"\n",
{},
RPCResult{RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::STR, "walletname", "the wallet name"},
}},
RPCExamples{HelpExampleCli("listwallets", "") +
HelpExampleRpc("listwallets", "")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
UniValue obj(UniValue::VARR);
for (const std::shared_ptr<CWallet> &wallet : GetWallets()) {
LOCK(wallet->cs_wallet);
obj.push_back(wallet->GetName());
}
return obj;
},
};
}
static RPCHelpMan loadwallet() {
return RPCHelpMan{
"loadwallet",
"Loads a wallet from a wallet file or directory."
"\nNote that all wallet command-line options used when starting "
"bitcoind will be"
"\napplied to the new wallet (eg -rescan, etc).\n",
{
{"filename", RPCArg::Type::STR, RPCArg::Optional::NO,
"The wallet directory or .dat file."},
{"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED,
"Save wallet name to persistent settings and load on startup. "
"True to add wallet to startup list, false to remove, null to "
"leave unchanged."},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "name",
"The wallet name if loaded successfully."},
{RPCResult::Type::STR, "warning",
"Warning message if wallet was not loaded cleanly."},
}},
RPCExamples{HelpExampleCli("loadwallet", "\"test.dat\"") +
HelpExampleRpc("loadwallet", "\"test.dat\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
WalletContext &context = EnsureWalletContext(request.context);
const std::string name(request.params[0].get_str());
auto [wallet, warnings] =
LoadWalletHelper(context, request.params[1], name);
UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName());
obj.pushKV("warning", Join(warnings, Untranslated("\n")).original);
return obj;
},
};
}
static RPCHelpMan setwalletflag() {
std::string flags = "";
for (auto &it : WALLET_FLAG_MAP) {
if (it.second & MUTABLE_WALLET_FLAGS) {
flags += (flags == "" ? "" : ", ") + it.first;
}
}
return RPCHelpMan{
"setwalletflag",
"Change the state of the given wallet flag for a wallet.\n",
{
{"flag", RPCArg::Type::STR, RPCArg::Optional::NO,
"The name of the flag to change. Current available flags: " +
flags},
{"value", RPCArg::Type::BOOL, RPCArg::Default{true},
"The new state."},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "flag_name",
"The name of the flag that was modified"},
{RPCResult::Type::BOOL, "flag_state",
"The new state of the flag"},
{RPCResult::Type::STR, "warnings",
"Any warnings associated with the change"},
}},
RPCExamples{HelpExampleCli("setwalletflag", "avoid_reuse") +
HelpExampleRpc("setwalletflag", "\"avoid_reuse\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
std::string flag_str = request.params[0].get_str();
bool value =
request.params[1].isNull() || request.params[1].get_bool();
if (!WALLET_FLAG_MAP.count(flag_str)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Unknown wallet flag: %s", flag_str));
}
auto flag = WALLET_FLAG_MAP.at(flag_str);
if (!(flag & MUTABLE_WALLET_FLAGS)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Wallet flag is immutable: %s", flag_str));
}
UniValue res(UniValue::VOBJ);
if (pwallet->IsWalletFlagSet(flag) == value) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Wallet flag is already set to %s: %s",
value ? "true" : "false", flag_str));
}
res.pushKV("flag_name", flag_str);
res.pushKV("flag_state", value);
if (value) {
pwallet->SetWalletFlag(flag);
} else {
pwallet->UnsetWalletFlag(flag);
}
if (flag && value && WALLET_FLAG_CAVEATS.count(flag)) {
res.pushKV("warnings", WALLET_FLAG_CAVEATS.at(flag));
}
return res;
},
};
}
static RPCHelpMan createwallet() {
return RPCHelpMan{
"createwallet",
"Creates and loads a new wallet.\n",
{
{"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO,
"The name for the new wallet. If this is a path, the wallet will "
"be created at the path location."},
{"disable_private_keys", RPCArg::Type::BOOL, RPCArg::Default{false},
"Disable the possibility of private keys (only watchonlys are "
"possible in this mode)."},
{"blank", RPCArg::Type::BOOL, RPCArg::Default{false},
"Create a blank wallet. A blank wallet has no keys or HD seed. "
"One can be set using sethdseed."},
{"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"Encrypt the wallet with this passphrase."},
{"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{false},
"Keep track of coin reuse, and treat dirty and clean coins "
"differently with privacy considerations in mind."},
{"descriptors", RPCArg::Type::BOOL, RPCArg::Default{false},
"Create a native descriptor wallet. The wallet will use "
"descriptors internally to handle address creation"},
{"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED,
"Save wallet name to persistent settings and load on startup. "
"True to add wallet to startup list, false to remove, null to "
"leave unchanged."},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "name",
"The wallet name if created successfully. If the wallet "
"was created using a full path, the wallet_name will be "
"the full path."},
{RPCResult::Type::STR, "warning",
"Warning message if wallet was not loaded cleanly."},
}},
RPCExamples{
HelpExampleCli("createwallet", "\"testwallet\"") +
HelpExampleRpc("createwallet", "\"testwallet\"") +
HelpExampleCliNamed("createwallet", {{"wallet_name", "descriptors"},
{"avoid_reuse", true},
{"descriptors", true},
{"load_on_startup", true}}) +
HelpExampleRpcNamed("createwallet", {{"wallet_name", "descriptors"},
{"avoid_reuse", true},
{"descriptors", true},
{"load_on_startup", true}})},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
WalletContext &context = EnsureWalletContext(request.context);
uint64_t flags = 0;
if (!request.params[1].isNull() && request.params[1].get_bool()) {
flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS;
}
if (!request.params[2].isNull() && request.params[2].get_bool()) {
flags |= WALLET_FLAG_BLANK_WALLET;
}
SecureString passphrase;
passphrase.reserve(100);
std::vector<bilingual_str> warnings;
if (!request.params[3].isNull()) {
passphrase = request.params[3].get_str().c_str();
if (passphrase.empty()) {
// Empty string means unencrypted
warnings.emplace_back(Untranslated(
"Empty string given as passphrase, wallet will "
"not be encrypted."));
}
}
if (!request.params[4].isNull() && request.params[4].get_bool()) {
flags |= WALLET_FLAG_AVOID_REUSE;
}
if (!request.params[5].isNull() && request.params[5].get_bool()) {
flags |= WALLET_FLAG_DESCRIPTORS;
warnings.emplace_back(Untranslated(
"Wallet is an experimental descriptor wallet"));
}
DatabaseOptions options;
DatabaseStatus status;
options.require_create = true;
options.create_flags = flags;
options.create_passphrase = passphrase;
bilingual_str error;
std::optional<bool> load_on_start =
request.params[6].isNull()
? std::nullopt
: std::make_optional<bool>(request.params[6].get_bool());
std::shared_ptr<CWallet> wallet =
CreateWallet(*context.chain, request.params[0].get_str(),
load_on_start, options, status, error, warnings);
if (!wallet) {
RPCErrorCode code = status == DatabaseStatus::FAILED_ENCRYPT
? RPC_WALLET_ENCRYPTION_FAILED
: RPC_WALLET_ERROR;
throw JSONRPCError(code, error.original);
}
UniValue obj(UniValue::VOBJ);
obj.pushKV("name", wallet->GetName());
obj.pushKV("warning", Join(warnings, Untranslated("\n")).original);
return obj;
},
};
}
static RPCHelpMan unloadwallet() {
return RPCHelpMan{
"unloadwallet",
"Unloads the wallet referenced by the request endpoint otherwise "
"unloads the wallet specified in the argument.\n"
"Specifying the wallet name on a wallet endpoint is invalid.",
{
{"wallet_name", RPCArg::Type::STR,
RPCArg::DefaultHint{"the wallet name from the RPC request"},
"The name of the wallet to unload."},
{"load_on_startup", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED,
"Save wallet name to persistent settings and load on startup. "
"True to add wallet to startup list, false to remove, null to "
"leave unchanged."},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "warning",
"Warning message if wallet was not unloaded cleanly."},
}},
RPCExamples{HelpExampleCli("unloadwallet", "wallet_name") +
HelpExampleRpc("unloadwallet", "wallet_name")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::string wallet_name;
if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) {
if (!request.params[0].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Cannot unload the requested wallet");
}
} else {
wallet_name = request.params[0].get_str();
}
std::shared_ptr<CWallet> wallet = GetWallet(wallet_name);
if (!wallet) {
throw JSONRPCError(
RPC_WALLET_NOT_FOUND,
"Requested wallet does not exist or is not loaded");
}
// Release the "main" shared pointer and prevent further
// notifications. Note that any attempt to load the same wallet
// would fail until the wallet is destroyed (see CheckUniqueFileid).
std::vector<bilingual_str> warnings;
std::optional<bool> load_on_start{self.MaybeArg<bool>(1)};
if (!RemoveWallet(wallet, load_on_start, warnings)) {
throw JSONRPCError(RPC_MISC_ERROR,
"Requested wallet already unloaded");
}
UnloadWallet(std::move(wallet));
UniValue result(UniValue::VOBJ);
result.pushKV("warning",
Join(warnings, Untranslated("\n")).original);
return result;
},
};
}
static RPCHelpMan listunspent() {
const auto &ticker = Currency::get().ticker;
return RPCHelpMan{
"listunspent",
"Returns array of unspent transaction outputs\n"
"with between minconf and maxconf (inclusive) confirmations.\n"
"Optionally filter to only include txouts paid to specified "
"addresses.\n",
{
{"minconf", RPCArg::Type::NUM, RPCArg::Default{1},
"The minimum confirmations to filter"},
{"maxconf", RPCArg::Type::NUM, RPCArg::Default{9999999},
"The maximum confirmations to filter"},
{
"addresses",
RPCArg::Type::ARR,
RPCArg::Default{UniValue::VARR},
"The bitcoin addresses to filter",
{
{"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"bitcoin address"},
},
},
{"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{true},
"Include outputs that are not safe to spend\n"
" See description of \"safe\" attribute below."},
{"query_options",
RPCArg::Type::OBJ_NAMED_PARAMS,
RPCArg::Optional::OMITTED,
"JSON with query options",
{
{"minimumAmount", RPCArg::Type::AMOUNT,
RPCArg::Default{FormatMoney(Amount::zero())},
"Minimum value of each UTXO in " + ticker + ""},
{"maximumAmount", RPCArg::Type::AMOUNT,
RPCArg::DefaultHint{"unlimited"},
"Maximum value of each UTXO in " + ticker + ""},
{"maximumCount", RPCArg::Type::NUM,
RPCArg::DefaultHint{"unlimited"}, "Maximum number of UTXOs"},
{"minimumSumAmount", RPCArg::Type::AMOUNT,
RPCArg::DefaultHint{"unlimited"},
"Minimum sum value of all UTXOs in " + ticker + ""},
},
RPCArgOptions{.oneline_description = "query_options"}},
},
RPCResult{
RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR_HEX, "txid", "the transaction id"},
{RPCResult::Type::NUM, "vout", "the vout value"},
{RPCResult::Type::STR, "address", "the bitcoin address"},
{RPCResult::Type::STR, "label",
"The associated label, or \"\" for the default label"},
{RPCResult::Type::STR, "scriptPubKey", "the script key"},
{RPCResult::Type::STR_AMOUNT, "amount",
"the transaction output amount in " + ticker},
{RPCResult::Type::NUM, "confirmations",
"The number of confirmations"},
{RPCResult::Type::NUM, "ancestorcount",
/* optional */ true,
"DEPRECATED: The number of in-mempool ancestor "
"transactions, including this one (if transaction is in "
"the mempool). Only displayed if the "
"-deprecatedrpc=mempool_ancestors_descendants option is "
"set"},
{RPCResult::Type::NUM, "ancestorsize", /* optional */ true,
"DEPRECATED: The virtual transaction size of in-mempool "
" ancestors, including this one (if transaction is in "
"the mempool). Only displayed if the "
"-deprecatedrpc=mempool_ancestors_descendants option is "
"set"},
{RPCResult::Type::STR_AMOUNT, "ancestorfees",
/* optional */ true,
"DEPRECATED: The total fees of in-mempool ancestors "
"(including this one) with fee deltas used for mining "
"priority in " +
ticker +
" (if transaction is in the mempool). Only "
"displayed if the "
"-deprecatedrpc=mempool_ancestors_descendants option "
"is "
"set"},
{RPCResult::Type::STR_HEX, "redeemScript",
"The redeemScript if scriptPubKey is P2SH"},
{RPCResult::Type::BOOL, "spendable",
"Whether we have the private keys to spend this output"},
{RPCResult::Type::BOOL, "solvable",
"Whether we know how to spend this output, ignoring the "
"lack of keys"},
{RPCResult::Type::BOOL, "reused",
"(only present if avoid_reuse is set) Whether this "
"output is reused/dirty (sent to an address that was "
"previously spent from)"},
{RPCResult::Type::STR, "desc",
"(only when solvable) A descriptor for spending this "
"output"},
{RPCResult::Type::BOOL, "safe",
"Whether this output is considered safe to spend. "
"Unconfirmed transactions\n"
"from outside keys are considered unsafe\n"
"and are not eligible for spending by fundrawtransaction "
"and sendtoaddress."},
}},
}},
RPCExamples{
HelpExampleCli("listunspent", "") +
HelpExampleCli("listunspent",
"6 9999999 "
"\"[\\\"1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\","
"\\\"1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"") +
HelpExampleRpc("listunspent",
"6, 9999999 "
"\"[\\\"1PGFqEzfmQch1gKD3ra4k18PNj3tTUUSqg\\\","
"\\\"1LtvqCaApEdUGFkpKMM4MstjcaL4dKg8SP\\\"]\"") +
HelpExampleCli(
"listunspent",
"6 9999999 '[]' true '{ \"minimumAmount\": 0.005 }'") +
HelpExampleRpc(
"listunspent",
"6, 9999999, [] , true, { \"minimumAmount\": 0.005 } ")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
int nMinDepth = 1;
if (!request.params[0].isNull()) {
nMinDepth = request.params[0].getInt<int>();
}
int nMaxDepth = 9999999;
if (!request.params[1].isNull()) {
nMaxDepth = request.params[1].getInt<int>();
}
std::set<CTxDestination> destinations;
if (!request.params[2].isNull()) {
UniValue inputs = request.params[2].get_array();
for (size_t idx = 0; idx < inputs.size(); idx++) {
const UniValue &input = inputs[idx];
CTxDestination dest = DecodeDestination(
input.get_str(), wallet->GetChainParams());
if (!IsValidDestination(dest)) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
std::string("Invalid Bitcoin address: ") +
input.get_str());
}
if (!destinations.insert(dest).second) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
std::string(
"Invalid parameter, duplicated address: ") +
input.get_str());
}
}
}
bool include_unsafe = true;
if (!request.params[3].isNull()) {
include_unsafe = request.params[3].get_bool();
}
Amount nMinimumAmount = Amount::zero();
Amount nMaximumAmount = MAX_MONEY;
Amount nMinimumSumAmount = MAX_MONEY;
uint64_t nMaximumCount = 0;
if (!request.params[4].isNull()) {
const UniValue &options = request.params[4].get_obj();
RPCTypeCheckObj(
options,
{
{"minimumAmount", UniValueType()},
{"maximumAmount", UniValueType()},
{"minimumSumAmount", UniValueType()},
{"maximumCount", UniValueType(UniValue::VNUM)},
},
true, true);
if (options.exists("minimumAmount")) {
nMinimumAmount = AmountFromValue(options["minimumAmount"]);
}
if (options.exists("maximumAmount")) {
nMaximumAmount = AmountFromValue(options["maximumAmount"]);
}
if (options.exists("minimumSumAmount")) {
nMinimumSumAmount =
AmountFromValue(options["minimumSumAmount"]);
}
if (options.exists("maximumCount")) {
nMaximumCount = options["maximumCount"].getInt<int64_t>();
}
}
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
UniValue results(UniValue::VARR);
std::vector<COutput> vecOutputs;
{
CCoinControl cctl;
cctl.m_avoid_address_reuse = false;
cctl.m_min_depth = nMinDepth;
cctl.m_max_depth = nMaxDepth;
cctl.m_include_unsafe_inputs = include_unsafe;
LOCK(pwallet->cs_wallet);
AvailableCoins(*pwallet, vecOutputs, &cctl, nMinimumAmount,
nMaximumAmount, nMinimumSumAmount,
nMaximumCount);
}
LOCK(pwallet->cs_wallet);
const bool avoid_reuse =
pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE);
for (const COutput &out : vecOutputs) {
CTxDestination address;
const CScript &scriptPubKey =
out.tx->tx->vout[out.i].scriptPubKey;
bool fValidAddress = ExtractDestination(scriptPubKey, address);
bool reused =
avoid_reuse && pwallet->IsSpentKey(out.tx->GetId(), out.i);
if (destinations.size() &&
(!fValidAddress || !destinations.count(address))) {
continue;
}
UniValue entry(UniValue::VOBJ);
entry.pushKV("txid", out.tx->GetId().GetHex());
entry.pushKV("vout", out.i);
if (fValidAddress) {
entry.pushKV("address", EncodeDestination(address, config));
const auto *address_book_entry =
pwallet->FindAddressBookEntry(address);
if (address_book_entry) {
entry.pushKV("label", address_book_entry->GetLabel());
}
std::unique_ptr<SigningProvider> provider =
pwallet->GetSolvingProvider(scriptPubKey);
if (provider) {
if (scriptPubKey.IsPayToScriptHash()) {
const CScriptID &hash =
CScriptID(std::get<ScriptHash>(address));
CScript redeemScript;
if (provider->GetCScript(hash, redeemScript)) {
entry.pushKV("redeemScript",
HexStr(redeemScript));
}
}
}
}
entry.pushKV("scriptPubKey", HexStr(scriptPubKey));
entry.pushKV("amount", out.tx->tx->vout[out.i].nValue);
entry.pushKV("confirmations", out.nDepth);
entry.pushKV("spendable", out.fSpendable);
entry.pushKV("solvable", out.fSolvable);
if (out.fSolvable) {
std::unique_ptr<SigningProvider> provider =
pwallet->GetSolvingProvider(scriptPubKey);
if (provider) {
auto descriptor =
InferDescriptor(scriptPubKey, *provider);
entry.pushKV("desc", descriptor->ToString());
}
}
if (avoid_reuse) {
entry.pushKV("reused", reused);
}
entry.pushKV("safe", out.fSafe);
results.push_back(entry);
}
return results;
},
};
}
void FundTransaction(CWallet *const pwallet, CMutableTransaction &tx,
Amount &fee_out, int &change_position,
const UniValue &options, CCoinControl &coinControl) {
// Make sure the results are valid at least up to the most recent block
// the user could have gotten from another RPC command prior to now
pwallet->BlockUntilSyncedToCurrentChain();
change_position = -1;
bool lockUnspents = false;
UniValue subtractFeeFromOutputs;
std::set<int> setSubtractFeeFromOutputs;
if (!options.isNull()) {
if (options.type() == UniValue::VBOOL) {
// backward compatibility bool only fallback
coinControl.fAllowWatchOnly = options.get_bool();
} else {
RPCTypeCheckObj(
options,
{
{"add_inputs", UniValueType(UniValue::VBOOL)},
{"include_unsafe", UniValueType(UniValue::VBOOL)},
{"add_to_wallet", UniValueType(UniValue::VBOOL)},
{"changeAddress", UniValueType(UniValue::VSTR)},
{"change_address", UniValueType(UniValue::VSTR)},
{"changePosition", UniValueType(UniValue::VNUM)},
{"change_position", UniValueType(UniValue::VNUM)},
{"includeWatching", UniValueType(UniValue::VBOOL)},
{"include_watching", UniValueType(UniValue::VBOOL)},
{"inputs", UniValueType(UniValue::VARR)},
{"lockUnspents", UniValueType(UniValue::VBOOL)},
{"lock_unspents", UniValueType(UniValue::VBOOL)},
{"locktime", UniValueType(UniValue::VNUM)},
// will be checked below
{"feeRate", UniValueType()},
{"fee_rate", UniValueType()},
{"psbt", UniValueType(UniValue::VBOOL)},
{"subtractFeeFromOutputs", UniValueType(UniValue::VARR)},
{"subtract_fee_from_outputs", UniValueType(UniValue::VARR)},
},
true, true);
if (options.exists("add_inputs")) {
coinControl.m_add_inputs = options["add_inputs"].get_bool();
}
if (options.exists("changeAddress") ||
options.exists("change_address")) {
const std::string change_address_str =
(options.exists("change_address")
? options["change_address"]
: options["changeAddress"])
.get_str();
CTxDestination dest = DecodeDestination(
change_address_str, pwallet->GetChainParams());
if (!IsValidDestination(dest)) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Change address must be a valid bitcoin address");
}
coinControl.destChange = dest;
}
if (options.exists("changePosition") ||
options.exists("change_position")) {
change_position = (options.exists("change_position")
? options["change_position"]
: options["changePosition"])
.getInt<int>();
}
const UniValue include_watching_option =
options.exists("include_watching") ? options["include_watching"]
: options["includeWatching"];
coinControl.fAllowWatchOnly =
ParseIncludeWatchonly(include_watching_option, *pwallet);
if (options.exists("lockUnspents") ||
options.exists("lock_unspents")) {
lockUnspents =
(options.exists("lock_unspents") ? options["lock_unspents"]
: options["lockUnspents"])
.get_bool();
}
if (options.exists("include_unsafe")) {
coinControl.m_include_unsafe_inputs =
options["include_unsafe"].get_bool();
}
if (options.exists("feeRate") || options.exists("fee_rate")) {
coinControl.m_feerate = CFeeRate(AmountFromValue(
options.exists("fee_rate") ? options["fee_rate"]
: options["feeRate"]));
coinControl.fOverrideFeeRate = true;
}
if (options.exists("subtractFeeFromOutputs") ||
options.exists("subtract_fee_from_outputs")) {
subtractFeeFromOutputs =
(options.exists("subtract_fee_from_outputs")
? options["subtract_fee_from_outputs"]
: options["subtractFeeFromOutputs"])
.get_array();
}
}
} else {
// if options is null and not a bool
coinControl.fAllowWatchOnly =
ParseIncludeWatchonly(NullUniValue, *pwallet);
}
if (tx.vout.size() == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"TX must have at least one output");
}
if (change_position != -1 &&
(change_position < 0 ||
(unsigned int)change_position > tx.vout.size())) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"changePosition out of bounds");
}
for (size_t idx = 0; idx < subtractFeeFromOutputs.size(); idx++) {
int pos = subtractFeeFromOutputs[idx].getInt<int>();
if (setSubtractFeeFromOutputs.count(pos)) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Invalid parameter, duplicated position: %d", pos));
}
if (pos < 0) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Invalid parameter, negative position: %d", pos));
}
if (pos >= int(tx.vout.size())) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf("Invalid parameter, position too large: %d", pos));
}
setSubtractFeeFromOutputs.insert(pos);
}
bilingual_str error;
if (!FundTransaction(*pwallet, tx, fee_out, change_position, error,
lockUnspents, setSubtractFeeFromOutputs,
coinControl)) {
throw JSONRPCError(RPC_WALLET_ERROR, error.original);
}
}
static RPCHelpMan fundrawtransaction() {
const auto &ticker = Currency::get().ticker;
return RPCHelpMan{
"fundrawtransaction",
"If the transaction has no inputs, they will be automatically selected "
"to meet its out value.\n"
"It will add at most one change output to the outputs.\n"
"No existing outputs will be modified unless "
"\"subtractFeeFromOutputs\" is specified.\n"
"Note that inputs which were signed may need to be resigned after "
"completion since in/outputs have been added.\n"
"The inputs added will not be signed, use signrawtransactionwithkey or "
"signrawtransactionwithwallet for that.\n"
"Note that all existing inputs must have their previous output "
"transaction be in the wallet.\n"
"Note that all inputs selected must be of standard form and P2SH "
"scripts must be\n"
"in the wallet using importaddress or addmultisigaddress (to calculate "
"fees).\n"
"You can see whether this is the case by checking the \"solvable\" "
"field in the listunspent output.\n"
"Only pay-to-pubkey, multisig, and P2SH versions thereof are currently "
"supported for watch-only\n",
{
{"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
"The hex string of the raw transaction"},
{"options",
RPCArg::Type::OBJ_NAMED_PARAMS,
RPCArg::Optional::OMITTED,
"For backward compatibility: passing in a true instead of an "
"object will result in {\"includeWatching\":true}",
{
{"add_inputs", RPCArg::Type::BOOL, RPCArg::Default{true},
"For a transaction with existing inputs, automatically "
"include more if they are not enough."},
{"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{false},
"Include inputs that are not safe to spend (unconfirmed "
"transactions from outside keys).\n"
"Warning: the resulting transaction may become invalid if "
"one of the unsafe inputs disappears.\n"
"If that happens, you will need to fund the transaction with "
"different inputs and republish it."},
{"changeAddress", RPCArg::Type::STR,
RPCArg::DefaultHint{"pool address"},
"The bitcoin address to receive the change"},
{"changePosition", RPCArg::Type::NUM,
RPCArg::DefaultHint{"random"},
"The index of the change output"},
{"includeWatching", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Also select inputs which are watch only.\n"
"Only solvable inputs can be used. Watch-only destinations "
"are solvable if the public key and/or output script was "
"imported,\n"
"e.g. with 'importpubkey' or 'importmulti' with the "
"'pubkeys' or 'desc' field."},
{"lockUnspents", RPCArg::Type::BOOL, RPCArg::Default{false},
"Lock selected unspent outputs"},
{"feeRate", RPCArg::Type::AMOUNT,
RPCArg::DefaultHint{
"not set: makes wallet determine the fee"},
"Set a specific fee rate in " + ticker + "/kB",
RPCArgOptions{.also_positional = true}},
{
"subtractFeeFromOutputs",
RPCArg::Type::ARR,
RPCArg::Default{UniValue::VARR},
"The integers.\n"
" The fee will be equally "
"deducted from the amount of each specified output.\n"
" Those recipients will "
"receive less bitcoins than you enter in their "
"corresponding amount field.\n"
" If no outputs are "
"specified here, the sender pays the fee.",
{
{"vout_index", RPCArg::Type::NUM,
RPCArg::Optional::OMITTED,
"The zero-based output index, before a change output "
"is added."},
},
},
},
RPCArgOptions{.skip_type_check = true,
.oneline_description = "options"}},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR_HEX, "hex",
"The resulting raw transaction (hex-encoded string)"},
{RPCResult::Type::STR_AMOUNT, "fee",
"Fee in " + ticker + " the resulting transaction pays"},
{RPCResult::Type::NUM, "changepos",
"The position of the added change output, or -1"},
}},
RPCExamples{
"\nCreate a transaction with no inputs\n" +
HelpExampleCli("createrawtransaction",
"\"[]\" \"{\\\"myaddress\\\":10000}\"") +
"\nAdd sufficient unsigned inputs to meet the output value\n" +
HelpExampleCli("fundrawtransaction", "\"rawtransactionhex\"") +
"\nSign the transaction\n" +
HelpExampleCli("signrawtransactionwithwallet",
"\"fundedtransactionhex\"") +
"\nSend the transaction\n" +
HelpExampleCli("sendrawtransaction", "\"signedtransactionhex\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
// parse hex string from parameter
CMutableTransaction tx;
if (!DecodeHexTx(tx, request.params[0].get_str())) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
"TX decode failed");
}
Amount fee;
int change_position;
CCoinControl coin_control;
// Automatically select (additional) coins. Can be overridden by
// options.add_inputs.
coin_control.m_add_inputs = true;
FundTransaction(pwallet, tx, fee, change_position,
request.params[1], coin_control);
UniValue result(UniValue::VOBJ);
result.pushKV("hex", EncodeHexTx(CTransaction(tx)));
result.pushKV("fee", fee);
result.pushKV("changepos", change_position);
return result;
},
};
}
RPCHelpMan signrawtransactionwithwallet() {
return RPCHelpMan{
"signrawtransactionwithwallet",
"Sign inputs for raw transaction (serialized, hex-encoded).\n"
"The second optional argument (may be null) is an array of previous "
"transaction outputs that\n"
"this transaction depends on but may not yet be in the block chain.\n" +
HELP_REQUIRING_PASSPHRASE,
{
{"hexstring", RPCArg::Type::STR, RPCArg::Optional::NO,
"The transaction hex string"},
{
"prevtxs",
RPCArg::Type::ARR,
RPCArg::Optional::OMITTED,
"The previous dependent transaction outputs",
{
{
"",
RPCArg::Type::OBJ,
RPCArg::Optional::OMITTED,
"",
{
{"txid", RPCArg::Type::STR_HEX,
RPCArg::Optional::NO, "The transaction id"},
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO,
"The output number"},
{"scriptPubKey", RPCArg::Type::STR_HEX,
RPCArg::Optional::NO, "script key"},
{"redeemScript", RPCArg::Type::STR_HEX,
RPCArg::Optional::OMITTED, "(required for P2SH)"},
{"amount", RPCArg::Type::AMOUNT,
RPCArg::Optional::NO, "The amount spent"},
},
},
},
},
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"ALL|FORKID"},
"The signature hash type. Must be one of\n"
" \"ALL|FORKID\"\n"
" \"NONE|FORKID\"\n"
" \"SINGLE|FORKID\"\n"
" \"ALL|FORKID|ANYONECANPAY\"\n"
" \"NONE|FORKID|ANYONECANPAY\"\n"
" \"SINGLE|FORKID|ANYONECANPAY\""},
},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR_HEX, "hex",
"The hex-encoded raw transaction with signature(s)"},
{RPCResult::Type::BOOL, "complete",
"If the transaction has a complete set of signatures"},
{RPCResult::Type::ARR,
"errors",
/* optional */ true,
"Script verification errors (if there are any)",
{
{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR_HEX, "txid",
"The hash of the referenced, previous transaction"},
{RPCResult::Type::NUM, "vout",
"The index of the output to spent and used as "
"input"},
{RPCResult::Type::STR_HEX, "scriptSig",
"The hex-encoded signature script"},
{RPCResult::Type::NUM, "sequence",
"Script sequence number"},
{RPCResult::Type::STR, "error",
"Verification or signing error related to the "
"input"},
}},
}},
}},
RPCExamples{
HelpExampleCli("signrawtransactionwithwallet", "\"myhex\"") +
HelpExampleRpc("signrawtransactionwithwallet", "\"myhex\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
CMutableTransaction mtx;
if (!DecodeHexTx(mtx, request.params[0].get_str())) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
"TX decode failed");
}
// Sign the transaction
LOCK(pwallet->cs_wallet);
EnsureWalletIsUnlocked(pwallet);
// Fetch previous transactions (inputs):
std::map<COutPoint, Coin> coins;
for (const CTxIn &txin : mtx.vin) {
// Create empty map entry keyed by prevout.
coins[txin.prevout];
}
pwallet->chain().findCoins(coins);
// Parse the prevtxs array
ParsePrevouts(request.params[1], nullptr, coins);
SigHashType nHashType = ParseSighashString(request.params[2]);
if (!nHashType.hasForkId()) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Signature must use SIGHASH_FORKID");
}
// Script verification errors
std::map<int, std::string> input_errors;
bool complete =
pwallet->SignTransaction(mtx, coins, nHashType, input_errors);
UniValue result(UniValue::VOBJ);
SignTransactionResultToJSON(mtx, complete, coins, input_errors,
result);
return result;
},
};
}
RPCHelpMan rescanblockchain() {
return RPCHelpMan{
"rescanblockchain",
"Rescan the local blockchain for wallet related transactions.\n"
"Note: Use \"getwalletinfo\" to query the scanning progress.\n",
{
{"start_height", RPCArg::Type::NUM, RPCArg::Default{0},
"block height where the rescan should start"},
{"stop_height", RPCArg::Type::NUM, RPCArg::Optional::OMITTED,
"the last block height that should be scanned"},
},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::NUM, "start_height",
"The block height where the rescan started (the requested "
"height or 0)"},
{RPCResult::Type::NUM, "stop_height",
"The height of the last rescanned block. May be null in rare "
"cases if there was a reorg and the call didn't scan any "
"blocks because they were already scanned in the background."},
}},
RPCExamples{HelpExampleCli("rescanblockchain", "100000 120000") +
HelpExampleRpc("rescanblockchain", "100000, 120000")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
WalletRescanReserver reserver(*pwallet);
if (!reserver.reserve()) {
throw JSONRPCError(RPC_WALLET_ERROR,
"Wallet is currently rescanning. Abort "
"existing rescan or wait.");
}
int start_height = 0;
std::optional<int> stop_height;
BlockHash start_block;
{
LOCK(pwallet->cs_wallet);
int tip_height = pwallet->GetLastBlockHeight();
if (!request.params[0].isNull()) {
start_height = request.params[0].getInt<int>();
if (start_height < 0 || start_height > tip_height) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Invalid start_height");
}
}
if (!request.params[1].isNull()) {
stop_height = request.params[1].getInt<int>();
if (*stop_height < 0 || *stop_height > tip_height) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Invalid stop_height");
} else if (*stop_height < start_height) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"stop_height must be greater than start_height");
}
}
// We can't rescan unavailable blocks, stop and throw an error
if (!pwallet->chain().hasBlocks(pwallet->GetLastBlockHash(),
start_height, stop_height)) {
if (pwallet->chain().havePruned() &&
pwallet->chain().getPruneHeight() >= start_height) {
throw JSONRPCError(RPC_MISC_ERROR,
"Can't rescan beyond pruned data. "
"Use RPC call getblockchaininfo to "
"determine your pruned height.");
}
if (pwallet->chain().hasAssumedValidChain()) {
throw JSONRPCError(
RPC_MISC_ERROR,
"Failed to rescan unavailable blocks likely due to "
"an in-progress assumeutxo background sync. Check "
"logs or getchainstates RPC for assumeutxo "
"background sync progress and try again later.");
}
throw JSONRPCError(
RPC_MISC_ERROR,
"Failed to rescan unavailable blocks, potentially "
"caused by data corruption. If the issue persists you "
"may want to reindex (see -reindex option).");
}
CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(
pwallet->GetLastBlockHash(), start_height,
FoundBlock().hash(start_block)));
}
CWallet::ScanResult result = pwallet->ScanForWalletTransactions(
start_block, start_height, stop_height, reserver,
true /* fUpdate */);
switch (result.status) {
case CWallet::ScanResult::SUCCESS:
break;
case CWallet::ScanResult::FAILURE:
throw JSONRPCError(
RPC_MISC_ERROR,
"Rescan failed. Potentially corrupted data files.");
case CWallet::ScanResult::USER_ABORT:
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted.");
// no default case, so the compiler can warn about missing
// cases
}
UniValue response(UniValue::VOBJ);
response.pushKV("start_height", start_height);
response.pushKV("stop_height", result.last_scanned_height
? *result.last_scanned_height
: UniValue());
return response;
},
};
}
class DescribeWalletAddressVisitor {
public:
const SigningProvider *const provider;
void ProcessSubScript(const CScript &subscript, UniValue &obj) const {
// Always present: script type and redeemscript
std::vector<std::vector<uint8_t>> solutions_data;
TxoutType which_type = Solver(subscript, solutions_data);
obj.pushKV("script", GetTxnOutputType(which_type));
obj.pushKV("hex", HexStr(subscript));
CTxDestination embedded;
if (ExtractDestination(subscript, embedded)) {
// Only when the script corresponds to an address.
UniValue subobj(UniValue::VOBJ);
UniValue detail = DescribeAddress(embedded);
subobj.pushKVs(detail);
UniValue wallet_detail = std::visit(*this, embedded);
subobj.pushKVs(wallet_detail);
subobj.pushKV("address", EncodeDestination(embedded, GetConfig()));
subobj.pushKV("scriptPubKey", HexStr(subscript));
// Always report the pubkey at the top level, so that
// `getnewaddress()['pubkey']` always works.
if (subobj.exists("pubkey")) {
obj.pushKV("pubkey", subobj["pubkey"]);
}
obj.pushKV("embedded", std::move(subobj));
} else if (which_type == TxoutType::MULTISIG) {
// Also report some information on multisig scripts (which do not
// have a corresponding address).
// TODO: abstract out the common functionality between this logic
// and ExtractDestinations.
obj.pushKV("sigsrequired", solutions_data[0][0]);
UniValue pubkeys(UniValue::VARR);
for (size_t i = 1; i < solutions_data.size() - 1; ++i) {
CPubKey key(solutions_data[i].begin(), solutions_data[i].end());
pubkeys.push_back(HexStr(key));
}
obj.pushKV("pubkeys", std::move(pubkeys));
}
}
explicit DescribeWalletAddressVisitor(const SigningProvider *_provider)
: provider(_provider) {}
UniValue operator()(const CNoDestination &dest) const {
return UniValue(UniValue::VOBJ);
}
UniValue operator()(const PKHash &pkhash) const {
CKeyID keyID(ToKeyID(pkhash));
UniValue obj(UniValue::VOBJ);
CPubKey vchPubKey;
if (provider && provider->GetPubKey(keyID, vchPubKey)) {
obj.pushKV("pubkey", HexStr(vchPubKey));
obj.pushKV("iscompressed", vchPubKey.IsCompressed());
}
return obj;
}
UniValue operator()(const ScriptHash &scripthash) const {
CScriptID scriptID(scripthash);
UniValue obj(UniValue::VOBJ);
CScript subscript;
if (provider && provider->GetCScript(scriptID, subscript)) {
ProcessSubScript(subscript, obj);
}
return obj;
}
};
static UniValue DescribeWalletAddress(const CWallet *const pwallet,
const CTxDestination &dest) {
UniValue ret(UniValue::VOBJ);
UniValue detail = DescribeAddress(dest);
CScript script = GetScriptForDestination(dest);
std::unique_ptr<SigningProvider> provider = nullptr;
if (pwallet) {
provider = pwallet->GetSolvingProvider(script);
}
ret.pushKVs(detail);
ret.pushKVs(std::visit(DescribeWalletAddressVisitor(provider.get()), dest));
return ret;
}
/** Convert CAddressBookData to JSON record. */
static UniValue AddressBookDataToJSON(const CAddressBookData &data,
const bool verbose) {
UniValue ret(UniValue::VOBJ);
if (verbose) {
ret.pushKV("name", data.GetLabel());
}
ret.pushKV("purpose", data.purpose);
return ret;
}
RPCHelpMan getaddressinfo() {
return RPCHelpMan{
"getaddressinfo",
"Return information about the given bitcoin address.\n"
"Some of the information will only be present if the address is in the "
"active wallet.\n",
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO,
"The bitcoin address for which to get information."},
},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "address",
"The bitcoin address validated."},
{RPCResult::Type::STR_HEX, "scriptPubKey",
"The hex-encoded scriptPubKey generated by the address."},
{RPCResult::Type::BOOL, "ismine", "If the address is yours."},
{RPCResult::Type::BOOL, "iswatchonly",
"If the address is watchonly."},
{RPCResult::Type::BOOL, "solvable",
"If we know how to spend coins sent to this address, ignoring "
"the possible lack of private keys."},
{RPCResult::Type::STR, "desc", /* optional */ true,
"A descriptor for spending coins sent to this address (only "
"when solvable)."},
{RPCResult::Type::BOOL, "isscript", "If the key is a script."},
{RPCResult::Type::BOOL, "ischange",
"If the address was used for change output."},
{RPCResult::Type::STR, "script", /* optional */ true,
"The output script type. Only if isscript is true and the "
"redeemscript is known. Possible\n"
" "
"types: nonstandard, pubkey, pubkeyhash, scripthash, "
"multisig, nulldata."},
{RPCResult::Type::STR_HEX, "hex", /* optional */ true,
"The redeemscript for the p2sh address."},
{RPCResult::Type::ARR,
"pubkeys",
/* optional */ true,
"Array of pubkeys associated with the known redeemscript "
"(only if script is multisig).",
{
{RPCResult::Type::STR, "pubkey", ""},
}},
{RPCResult::Type::NUM, "sigsrequired", /* optional */ true,
"The number of signatures required to spend multisig output "
"(only if script is multisig)."},
{RPCResult::Type::STR_HEX, "pubkey", /* optional */ true,
"The hex value of the raw public key for single-key addresses "
"(possibly embedded in P2SH)."},
{RPCResult::Type::OBJ,
"embedded",
/* optional */ true,
"Information about the address embedded in P2SH, if "
"relevant and known.",
{
{RPCResult::Type::ELISION, "",
"Includes all getaddressinfo output fields for the "
"embedded address excluding metadata (timestamp, "
"hdkeypath, hdseedid)\n"
"and relation to the wallet (ismine, iswatchonly)."},
}},
{RPCResult::Type::BOOL, "iscompressed", /* optional */ true,
"If the pubkey is compressed."},
{RPCResult::Type::NUM_TIME, "timestamp", /* optional */ true,
"The creation time of the key, if available, expressed in " +
UNIX_EPOCH_TIME + "."},
{RPCResult::Type::STR, "hdkeypath", /* optional */ true,
"The HD keypath, if the key is HD and available."},
{RPCResult::Type::STR_HEX, "hdseedid", /* optional */ true,
"The Hash160 of the HD seed."},
{RPCResult::Type::STR_HEX, "hdmasterfingerprint",
/* optional */ true, "The fingerprint of the master key."},
{RPCResult::Type::ARR,
"labels",
"Array of labels associated with the address. Currently "
"limited to one label but returned\n"
"as an array to keep the API stable if multiple labels are "
"enabled in the future.",
{
{RPCResult::Type::STR, "label name",
"Label name (defaults to \"\")."},
}},
}},
RPCExamples{HelpExampleCli("getaddressinfo", EXAMPLE_ADDRESS) +
HelpExampleRpc("getaddressinfo", EXAMPLE_ADDRESS)},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
UniValue ret(UniValue::VOBJ);
CTxDestination dest = DecodeDestination(request.params[0].get_str(),
wallet->GetChainParams());
// Make sure the destination is valid
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Invalid address");
}
std::string currentAddress = EncodeDestination(dest, config);
ret.pushKV("address", currentAddress);
CScript scriptPubKey = GetScriptForDestination(dest);
ret.pushKV("scriptPubKey", HexStr(scriptPubKey));
std::unique_ptr<SigningProvider> provider =
pwallet->GetSolvingProvider(scriptPubKey);
isminetype mine = pwallet->IsMine(dest);
ret.pushKV("ismine", bool(mine & ISMINE_SPENDABLE));
bool solvable = provider && IsSolvable(*provider, scriptPubKey);
ret.pushKV("solvable", solvable);
if (solvable) {
ret.pushKV(
"desc",
InferDescriptor(scriptPubKey, *provider)->ToString());
}
ret.pushKV("iswatchonly", bool(mine & ISMINE_WATCH_ONLY));
UniValue detail = DescribeWalletAddress(pwallet, dest);
ret.pushKVs(detail);
ret.pushKV("ischange", ScriptIsChange(*pwallet, scriptPubKey));
ScriptPubKeyMan *spk_man =
pwallet->GetScriptPubKeyMan(scriptPubKey);
if (spk_man) {
if (const std::unique_ptr<CKeyMetadata> meta =
spk_man->GetMetadata(dest)) {
ret.pushKV("timestamp", meta->nCreateTime);
if (meta->has_key_origin) {
ret.pushKV("hdkeypath",
WriteHDKeypath(meta->key_origin.path));
ret.pushKV("hdseedid", meta->hd_seed_id.GetHex());
ret.pushKV("hdmasterfingerprint",
HexStr(meta->key_origin.fingerprint));
}
}
}
// Return a `labels` array containing the label associated with the
// address, equivalent to the `label` field above. Currently only
// one label can be associated with an address, but we return an
// array so the API remains stable if we allow multiple labels to be
// associated with an address in the future.
UniValue labels(UniValue::VARR);
const auto *address_book_entry =
pwallet->FindAddressBookEntry(dest);
if (address_book_entry) {
labels.push_back(address_book_entry->GetLabel());
}
ret.pushKV("labels", std::move(labels));
return ret;
},
};
}
RPCHelpMan getaddressesbylabel() {
return RPCHelpMan{
"getaddressesbylabel",
"Returns the list of addresses assigned the specified label.\n",
{
{"label", RPCArg::Type::STR, RPCArg::Optional::NO, "The label."},
},
RPCResult{RPCResult::Type::OBJ_DYN,
"",
"json object with addresses as keys",
{
{RPCResult::Type::OBJ,
"address",
"Information about address",
{
{RPCResult::Type::STR, "purpose",
"Purpose of address (\"send\" for sending address, "
"\"receive\" for receiving address)"},
}},
}},
RPCExamples{HelpExampleCli("getaddressesbylabel", "\"tabby\"") +
HelpExampleRpc("getaddressesbylabel", "\"tabby\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
std::string label = LabelFromValue(request.params[0]);
// Find all addresses that have the given label
UniValue ret(UniValue::VOBJ);
std::set<std::string> addresses;
for (const std::pair<const CTxDestination, CAddressBookData> &item :
pwallet->m_address_book) {
if (item.second.IsChange()) {
continue;
}
if (item.second.GetLabel() == label) {
std::string address = EncodeDestination(item.first, config);
// CWallet::m_address_book is not expected to contain
// duplicate address strings, but build a separate set as a
// precaution just in case it does.
CHECK_NONFATAL(addresses.emplace(address).second);
// UniValue::pushKV checks if the key exists in O(N)
// and since duplicate addresses are unexpected (checked
// with std::set in O(log(N))), UniValue::pushKVEnd is used
// instead, which currently is O(1).
ret.pushKVEnd(address,
AddressBookDataToJSON(item.second, false));
}
}
if (ret.empty()) {
throw JSONRPCError(
RPC_WALLET_INVALID_LABEL_NAME,
std::string("No addresses with label " + label));
}
return ret;
},
};
}
RPCHelpMan listlabels() {
return RPCHelpMan{
"listlabels",
"Returns the list of all labels, or labels that are assigned to "
"addresses with a specific purpose.\n",
{
{"purpose", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
"Address purpose to list labels for ('send','receive'). An empty "
"string is the same as not providing this argument."},
},
RPCResult{RPCResult::Type::ARR,
"",
"",
{
{RPCResult::Type::STR, "label", "Label name"},
}},
RPCExamples{"\nList all labels\n" + HelpExampleCli("listlabels", "") +
"\nList labels that have receiving addresses\n" +
HelpExampleCli("listlabels", "receive") +
"\nList labels that have sending addresses\n" +
HelpExampleCli("listlabels", "send") +
"\nAs a JSON-RPC call\n" +
HelpExampleRpc("listlabels", "receive")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
LOCK(pwallet->cs_wallet);
std::string purpose;
if (!request.params[0].isNull()) {
purpose = request.params[0].get_str();
}
// Add to a set to sort by label name, then insert into Univalue
// array
std::set<std::string> label_set;
for (const std::pair<const CTxDestination, CAddressBookData>
&entry : pwallet->m_address_book) {
if (entry.second.IsChange()) {
continue;
}
if (purpose.empty() || entry.second.purpose == purpose) {
label_set.insert(entry.second.GetLabel());
}
}
UniValue ret(UniValue::VARR);
for (const std::string &name : label_set) {
ret.push_back(name);
}
return ret;
},
};
}
static RPCHelpMan send() {
return RPCHelpMan{
"send",
"EXPERIMENTAL warning: this call may be changed in future releases.\n"
"\nSend a transaction.\n",
{
{"outputs",
RPCArg::Type::ARR,
RPCArg::Optional::NO,
"A JSON array with outputs (key-value pairs), where none of "
"the keys are duplicated.\n"
"That is, each address can only appear once and there can only "
"be one 'data' object.\n"
"For convenience, a dictionary, which holds the key-value "
"pairs directly, is also accepted.",
{
{
"",
RPCArg::Type::OBJ_USER_KEYS,
RPCArg::Optional::OMITTED,
"",
{
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO,
"A key-value pair. The key (string) is the "
"bitcoin address, the value (float or string) is "
"the amount in " +
Currency::get().ticker + ""},
},
},
{
"",
RPCArg::Type::OBJ,
RPCArg::Optional::OMITTED,
"",
{
{"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
"A key-value pair. The key must be \"data\", the "
"value is hex-encoded data"},
},
},
},
RPCArgOptions{.skip_type_check = true}},
{"options",
RPCArg::Type::OBJ_NAMED_PARAMS,
RPCArg::Optional::OMITTED,
"",
{
{"add_inputs", RPCArg::Type::BOOL, RPCArg::Default{false},
"If inputs are specified, automatically include more if they "
"are not enough."},
{"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{false},
"Include inputs that are not safe to spend (unconfirmed "
"transactions from outside keys).\n"
"Warning: the resulting transaction may become invalid if "
"one of the unsafe inputs disappears.\n"
"If that happens, you will need to fund the transaction with "
"different inputs and republish it."},
{"add_to_wallet", RPCArg::Type::BOOL, RPCArg::Default{true},
"When false, returns a serialized transaction which will not "
"be added to the wallet or broadcast"},
{"change_address", RPCArg::Type::STR,
RPCArg::DefaultHint{"pool address"},
"The bitcoin address to receive the change"},
{"change_position", RPCArg::Type::NUM,
RPCArg::DefaultHint{"random"},
"The index of the change output"},
{"fee_rate", RPCArg::Type::AMOUNT,
RPCArg::DefaultHint{
"not set: makes wallet determine the fee"},
"Set a specific fee rate in " + Currency::get().ticker +
"/kB",
RPCArgOptions{.also_positional = true}},
{"include_watching", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Also select inputs which are watch only.\n"
"Only solvable inputs can be used. Watch-only destinations "
"are solvable if the public key and/or output script was "
"imported,\n"
"e.g. with 'importpubkey' or 'importmulti' with the "
"'pubkeys' or 'desc' field."},
{
"inputs",
RPCArg::Type::ARR,
RPCArg::Default{UniValue::VARR},
"Specify inputs instead of adding them automatically. A "
"JSON array of JSON objects",
{
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
"The transaction id"},
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO,
"The output number"},
{"sequence", RPCArg::Type::NUM, RPCArg::Optional::NO,
"The sequence number"},
},
},
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0},
"Raw locktime. Non-0 value also locktime-activates inputs"},
{"lock_unspents", RPCArg::Type::BOOL, RPCArg::Default{false},
"Lock selected unspent outputs"},
{"psbt", RPCArg::Type::BOOL, RPCArg::DefaultHint{"automatic"},
"Always return a PSBT, implies add_to_wallet=false."},
{
"subtract_fee_from_outputs",
RPCArg::Type::ARR,
RPCArg::Default{UniValue::VARR},
"Outputs to subtract the fee from, specified as integer "
"indices.\n"
"The fee will be equally deducted from the amount of each "
"specified output.\n"
"Those recipients will receive less bitcoins than you "
"enter in their corresponding amount field.\n"
"If no outputs are specified here, the sender pays the "
"fee.",
{
{"vout_index", RPCArg::Type::NUM,
RPCArg::Optional::OMITTED,
"The zero-based output index, before a change output "
"is added."},
},
},
},
RPCArgOptions{.oneline_description = "options"}},
},
RPCResult{
RPCResult::Type::OBJ,
"",
"",
{{RPCResult::Type::BOOL, "complete",
"If the transaction has a complete set of signatures"},
{RPCResult::Type::STR_HEX, "txid",
"The transaction id for the send. Only 1 transaction is created "
"regardless of the number of addresses."},
{RPCResult::Type::STR_HEX, "hex",
"If add_to_wallet is false, the hex-encoded raw transaction with "
"signature(s)"},
{RPCResult::Type::STR, "psbt",
"If more signatures are needed, or if add_to_wallet is false, "
"the base64-encoded (partially) signed transaction"}}},
RPCExamples{
""
"\nSend with a fee rate of 10 XEC/kB\n" +
HelpExampleCli("send", "'{\"" + EXAMPLE_ADDRESS +
"\": 100000}' '{\"fee_rate\": 10}'\n") +
"\nCreate a transaction with a specific input, and return "
"result without adding to wallet or broadcasting to the "
"network\n" +
HelpExampleCli("send",
"'{\"" + EXAMPLE_ADDRESS +
"\": 100000}' '{\"add_to_wallet\": "
"false, \"inputs\": "
"[{\"txid\":"
"\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b565"
"5e72f463568df1aadf0\", \"vout\":1}]}'")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
UniValue options = request.params[1];
if (options.exists("changeAddress")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use change_address");
}
if (options.exists("changePosition")) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Use change_position");
}
if (options.exists("includeWatching")) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Use include_watching");
}
if (options.exists("lockUnspents")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use lock_unspents");
}
if (options.exists("subtractFeeFromOutputs")) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Use subtract_fee_from_outputs");
}
if (options.exists("feeRate")) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Use fee_rate");
}
const bool psbt_opt_in =
options.exists("psbt") && options["psbt"].get_bool();
Amount fee;
int change_position;
CMutableTransaction rawTx = ConstructTransaction(
wallet->GetChainParams(), options["inputs"], request.params[0],
options["locktime"]);
CCoinControl coin_control;
// Automatically select coins, unless at least one is manually
// selected. Can be overridden by options.add_inputs.
coin_control.m_add_inputs = rawTx.vin.size() == 0;
FundTransaction(pwallet, rawTx, fee, change_position, options,
coin_control);
bool add_to_wallet = true;
if (options.exists("add_to_wallet")) {
add_to_wallet = options["add_to_wallet"].get_bool();
}
// Make a blank psbt
PartiallySignedTransaction psbtx(rawTx);
// Fill transaction with our data and sign
bool complete = true;
const TransactionError err = pwallet->FillPSBT(
psbtx, complete, SigHashType().withForkId(), true, false);
if (err != TransactionError::OK) {
throw JSONRPCTransactionError(err);
}
CMutableTransaction mtx;
complete = FinalizeAndExtractPSBT(psbtx, mtx);
UniValue result(UniValue::VOBJ);
if (psbt_opt_in || !complete || !add_to_wallet) {
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
result.pushKV("psbt", EncodeBase64(ssTx.str()));
}
if (complete) {
std::string err_string;
std::string hex = EncodeHexTx(CTransaction(mtx));
CTransactionRef tx(MakeTransactionRef(std::move(mtx)));
result.pushKV("txid", tx->GetHash().GetHex());
if (add_to_wallet && !psbt_opt_in) {
pwallet->CommitTransaction(tx, {}, {} /* orderForm */);
} else {
result.pushKV("hex", hex);
}
}
result.pushKV("complete", complete);
return result;
}};
}
static RPCHelpMan sethdseed() {
return RPCHelpMan{
"sethdseed",
"Set or generate a new HD wallet seed. Non-HD wallets will not be "
"upgraded to being a HD wallet. Wallets that are already\n"
"HD will have a new HD seed set so that new keys added to the keypool "
"will be derived from this new seed.\n"
"\nNote that you will need to MAKE A NEW BACKUP of your wallet after "
"setting the HD wallet seed.\n" +
HELP_REQUIRING_PASSPHRASE +
"Note: This command is only compatible with legacy wallets.\n",
{
{"newkeypool", RPCArg::Type::BOOL, RPCArg::Default{true},
"Whether to flush old unused addresses, including change "
"addresses, from the keypool and regenerate it.\n"
" If true, the next address from "
"getnewaddress and change address from getrawchangeaddress will "
"be from this new seed.\n"
" If false, addresses (including "
"change addresses if the wallet already had HD Chain Split "
"enabled) from the existing\n"
" keypool will be used until it has "
"been depleted."},
{"seed", RPCArg::Type::STR, RPCArg::DefaultHint{"random seed"},
"The WIF private key to use as the new HD seed.\n"
" The seed value can be retrieved "
"using the dumpwallet command. It is the private key marked "
"hdseed=1"},
},
RPCResult{RPCResult::Type::NONE, "", ""},
RPCExamples{HelpExampleCli("sethdseed", "") +
HelpExampleCli("sethdseed", "false") +
HelpExampleCli("sethdseed", "true \"wifkey\"") +
HelpExampleRpc("sethdseed", "true, \"wifkey\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
LegacyScriptPubKeyMan &spk_man =
EnsureLegacyScriptPubKeyMan(*pwallet, true);
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
throw JSONRPCError(RPC_WALLET_ERROR,
"Cannot set a HD seed to a wallet with "
"private keys disabled");
}
LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore);
// Do not do anything to non-HD wallets
if (!pwallet->CanSupportFeature(FEATURE_HD)) {
throw JSONRPCError(
RPC_WALLET_ERROR,
"Cannot set a HD seed on a non-HD wallet. Use the "
"upgradewallet RPC in order to upgrade a non-HD wallet "
"to HD");
}
EnsureWalletIsUnlocked(pwallet);
bool flush_key_pool = true;
if (!request.params[0].isNull()) {
flush_key_pool = request.params[0].get_bool();
}
CPubKey master_pub_key;
if (request.params[1].isNull()) {
master_pub_key = spk_man.GenerateNewSeed();
} else {
CKey key = DecodeSecret(request.params[1].get_str());
if (!key.IsValid()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
"Invalid private key");
}
if (HaveKey(spk_man, key)) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Already have this key (either as an HD seed or "
"as a loose private key)");
}
master_pub_key = spk_man.DeriveNewSeed(key);
}
spk_man.SetHDSeed(master_pub_key);
if (flush_key_pool) {
spk_man.NewKeyPool();
}
return NullUniValue;
},
};
}
static RPCHelpMan walletprocesspsbt() {
return RPCHelpMan{
"walletprocesspsbt",
"Update a PSBT with input information from our wallet and then sign "
"inputs that we can sign for." +
HELP_REQUIRING_PASSPHRASE,
{
{"psbt", RPCArg::Type::STR, RPCArg::Optional::NO,
"The transaction base64 string"},
{"sign", RPCArg::Type::BOOL, RPCArg::Default{true},
"Also sign the transaction when updating"},
{"sighashtype", RPCArg::Type::STR, RPCArg::Default{"ALL|FORKID"},
"The signature hash type to sign with if not specified by "
"the PSBT. Must be one of\n"
" \"ALL|FORKID\"\n"
" \"NONE|FORKID\"\n"
" \"SINGLE|FORKID\"\n"
" \"ALL|FORKID|ANYONECANPAY\"\n"
" \"NONE|FORKID|ANYONECANPAY\"\n"
" \"SINGLE|FORKID|ANYONECANPAY\""},
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true},
"Includes the BIP 32 derivation paths for public keys if we know "
"them"},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "psbt",
"The base64-encoded partially signed transaction"},
{RPCResult::Type::BOOL, "complete",
"If the transaction has a complete set of signatures"},
}},
RPCExamples{HelpExampleCli("walletprocesspsbt", "\"psbt\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
const CWallet *const pwallet = wallet.get();
// Unserialize the transaction
PartiallySignedTransaction psbtx;
std::string error;
if (!DecodeBase64PSBT(psbtx, request.params[0].get_str(), error)) {
throw JSONRPCError(RPC_DESERIALIZATION_ERROR,
strprintf("TX decode failed %s", error));
}
// Get the sighash type
SigHashType nHashType = ParseSighashString(request.params[2]);
if (!nHashType.hasForkId()) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Signature must use SIGHASH_FORKID");
}
// Fill transaction with our data and also sign
bool sign = request.params[1].isNull()
? true
: request.params[1].get_bool();
bool bip32derivs = request.params[3].isNull()
? true
: request.params[3].get_bool();
bool complete = true;
const TransactionError err = pwallet->FillPSBT(
psbtx, complete, nHashType, sign, bip32derivs);
if (err != TransactionError::OK) {
throw JSONRPCTransactionError(err);
}
UniValue result(UniValue::VOBJ);
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
result.pushKV("psbt", EncodeBase64(ssTx.str()));
result.pushKV("complete", complete);
return result;
},
};
}
static RPCHelpMan walletcreatefundedpsbt() {
const auto &ticker = Currency::get().ticker;
return RPCHelpMan{
"walletcreatefundedpsbt",
"Creates and funds a transaction in the Partially Signed Transaction "
"format.\n"
"Implements the Creator and Updater roles.\n",
{
{
"inputs",
RPCArg::Type::ARR,
RPCArg::Optional::OMITTED,
"Leave empty to add inputs automatically. See add_inputs "
"option.",
{
{
"",
RPCArg::Type::OBJ,
RPCArg::Optional::OMITTED,
"",
{
{"txid", RPCArg::Type::STR_HEX,
RPCArg::Optional::NO, "The transaction id"},
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO,
"The output number"},
{"sequence", RPCArg::Type::NUM,
RPCArg::DefaultHint{
"depends on the value of the 'locktime' and "
"'options.replaceable' arguments"},
"The sequence number"},
},
},
},
},
{"outputs",
RPCArg::Type::ARR,
RPCArg::Optional::NO,
"The outputs (key-value pairs), where none of "
"the keys are duplicated.\n"
"That is, each address can only appear once and there can only "
"be one 'data' object.\n"
"For compatibility reasons, a dictionary, which holds the "
"key-value pairs directly, is also\n"
" accepted as second parameter.",
{
{
"",
RPCArg::Type::OBJ_USER_KEYS,
RPCArg::Optional::OMITTED,
"",
{
{"address", RPCArg::Type::AMOUNT, RPCArg::Optional::NO,
"A key-value pair. The key (string) is the "
"bitcoin address, the value (float or string) is "
"the amount in " +
ticker + ""},
},
},
{
"",
RPCArg::Type::OBJ,
RPCArg::Optional::OMITTED,
"",
{
{"data", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
"A key-value pair. The key must be \"data\", the "
"value is hex-encoded data"},
},
},
},
RPCArgOptions{.skip_type_check = true}},
{"locktime", RPCArg::Type::NUM, RPCArg::Default{0},
"Raw locktime. Non-0 value also locktime-activates inputs\n"
" Allows this transaction to be "
"replaced by a transaction with higher fees. If provided, it is "
"an error if explicit sequence numbers are incompatible."},
{"options",
RPCArg::Type::OBJ_NAMED_PARAMS,
RPCArg::Optional::OMITTED,
"",
{
{"add_inputs", RPCArg::Type::BOOL, RPCArg::Default{false},
"If inputs are specified, automatically include more if they "
"are not enough."},
{"include_unsafe", RPCArg::Type::BOOL, RPCArg::Default{false},
"Include inputs that are not safe to spend (unconfirmed "
"transactions from outside keys).\n"
"Warning: the resulting transaction may become invalid if "
"one of the unsafe inputs disappears.\n"
"If that happens, you will need to fund the transaction with "
"different inputs and republish it."},
{"changeAddress", RPCArg::Type::STR,
RPCArg::DefaultHint{"pool address"},
"The bitcoin address to receive the change"},
{"changePosition", RPCArg::Type::NUM,
RPCArg::DefaultHint{"random"},
"The index of the change output"},
{"includeWatching", RPCArg::Type::BOOL,
RPCArg::DefaultHint{
"true for watch-only wallets, otherwise false"},
"Also select inputs which are watch only"},
{"lockUnspents", RPCArg::Type::BOOL, RPCArg::Default{false},
"Lock selected unspent outputs"},
{"feeRate", RPCArg::Type::AMOUNT,
RPCArg::DefaultHint{
"not set: makes wallet determine the fee"},
"Set a specific fee rate in " + ticker + "/kB",
RPCArgOptions{.also_positional = true}},
{
"subtractFeeFromOutputs",
RPCArg::Type::ARR,
RPCArg::Default{UniValue::VARR},
"The outputs to subtract the fee from.\n"
" The fee will be equally "
"deducted from the amount of each specified output.\n"
" Those recipients will "
"receive less bitcoins than you enter in their "
"corresponding amount field.\n"
" If no outputs are "
"specified here, the sender pays the fee.",
{
{"vout_index", RPCArg::Type::NUM,
RPCArg::Optional::OMITTED,
"The zero-based output index, before a change output "
"is added."},
},
},
},
RPCArgOptions{.oneline_description = "options"}},
{"bip32derivs", RPCArg::Type::BOOL, RPCArg::Default{true},
"Includes the BIP 32 derivation paths for public keys if we know "
"them"},
},
RPCResult{RPCResult::Type::OBJ,
"",
"",
{
{RPCResult::Type::STR, "psbt",
"The resulting raw transaction (base64-encoded string)"},
{RPCResult::Type::STR_AMOUNT, "fee",
"Fee in " + ticker + " the resulting transaction pays"},
{RPCResult::Type::NUM, "changepos",
"The position of the added change output, or -1"},
}},
RPCExamples{
"\nCreate a transaction with no inputs\n" +
HelpExampleCli("walletcreatefundedpsbt",
"\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" "
"\"[{\\\"data\\\":\\\"00010203\\\"}]\"")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
Amount fee;
int change_position;
CMutableTransaction rawTx = ConstructTransaction(
wallet->GetChainParams(), request.params[0], request.params[1],
request.params[2]);
CCoinControl coin_control;
// Automatically select coins, unless at least one is manually
// selected. Can be overridden by options.add_inputs.
coin_control.m_add_inputs = rawTx.vin.size() == 0;
FundTransaction(pwallet, rawTx, fee, change_position,
request.params[3], coin_control);
// Make a blank psbt
PartiallySignedTransaction psbtx(rawTx);
// Fill transaction with out data but don't sign
bool bip32derivs = request.params[4].isNull()
? true
: request.params[4].get_bool();
bool complete = true;
const TransactionError err =
pwallet->FillPSBT(psbtx, complete, SigHashType().withForkId(),
false, bip32derivs);
if (err != TransactionError::OK) {
throw JSONRPCTransactionError(err);
}
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
UniValue result(UniValue::VOBJ);
result.pushKV("psbt", EncodeBase64(ssTx.str()));
result.pushKV("fee", fee);
result.pushKV("changepos", change_position);
return result;
},
};
}
static RPCHelpMan upgradewallet() {
return RPCHelpMan{
"upgradewallet",
"Upgrade the wallet. Upgrades to the latest version if no "
"version number is specified\n"
"New keys may be generated and a new wallet backup will need to "
"be made.",
{{"version", RPCArg::Type::NUM, RPCArg::Default{int{FEATURE_LATEST}},
"The version number to upgrade to. Default is the latest "
"wallet version"}},
RPCResult{RPCResult::Type::NONE, "", ""},
RPCExamples{HelpExampleCli("upgradewallet", "200300") +
HelpExampleRpc("upgradewallet", "200300")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
EnsureWalletIsUnlocked(pwallet);
int version = 0;
if (!request.params[0].isNull()) {
version = request.params[0].getInt<int>();
}
bilingual_str error;
if (!pwallet->UpgradeWallet(version, error)) {
throw JSONRPCError(RPC_WALLET_ERROR, error.original);
}
return error.original;
},
};
}
RPCHelpMan signmessage();
static RPCHelpMan createwallettransaction() {
return RPCHelpMan{
"createwallettransaction",
"Create a transaction sending an amount to a given address.\n" +
HELP_REQUIRING_PASSPHRASE,
{
{"address", RPCArg::Type::STR, RPCArg::Optional::NO,
"The bitcoin address to send to."},
{"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO,
"The amount in " + Currency::get().ticker + " to send. eg 0.1"},
},
RPCResult{RPCResult::Type::STR_HEX, "txid", "The transaction id."},
RPCExamples{
HelpExampleCli("createwallettransaction",
"\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\" 100000") +
HelpExampleRpc("createwallettransaction",
"\"1M72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd\", 100000")},
[&](const RPCHelpMan &self, const Config &config,
const JSONRPCRequest &request) -> UniValue {
std::shared_ptr<CWallet> const wallet =
GetWalletForJSONRPCRequest(request);
if (!wallet) {
return NullUniValue;
}
CWallet *const pwallet = wallet.get();
// Make sure the results are valid at least up to the most recent
// block the user could have gotten from another RPC command prior
// to now
pwallet->BlockUntilSyncedToCurrentChain();
LOCK(pwallet->cs_wallet);
EnsureWalletIsUnlocked(pwallet);
UniValue address_amounts(UniValue::VOBJ);
const std::string address = request.params[0].get_str();
address_amounts.pushKV(address, request.params[1]);
UniValue subtractFeeFromAmount(UniValue::VARR);
std::vector<CRecipient> recipients;
ParseRecipients(address_amounts, subtractFeeFromAmount, recipients,
wallet->GetChainParams());
CCoinControl coin_control;
return SendMoney(pwallet, coin_control, recipients, {}, false);
},
};
}
Span<const CRPCCommand> GetWalletRPCCommands() {
// clang-format off
static const CRPCCommand commands[] = {
// category actor (function)
// ------------------ ----------------------
{ "rawtransactions", fundrawtransaction, },
{ "wallet", abandontransaction, },
{ "wallet", addmultisigaddress, },
{ "wallet", createwallet, },
{ "wallet", getaddressesbylabel, },
{ "wallet", getaddressinfo, },
{ "wallet", getbalance, },
{ "wallet", getnewaddress, },
{ "wallet", getrawchangeaddress, },
{ "wallet", getreceivedbyaddress, },
{ "wallet", getreceivedbylabel, },
{ "wallet", gettransaction, },
{ "wallet", getunconfirmedbalance, },
{ "wallet", getbalances, },
{ "wallet", getwalletinfo, },
{ "wallet", keypoolrefill, },
{ "wallet", listaddressgroupings, },
{ "wallet", listlabels, },
{ "wallet", listlockunspent, },
{ "wallet", listreceivedbyaddress, },
{ "wallet", listreceivedbylabel, },
{ "wallet", listsinceblock, },
{ "wallet", listtransactions, },
{ "wallet", listunspent, },
{ "wallet", listwalletdir, },
{ "wallet", listwallets, },
{ "wallet", loadwallet, },
{ "wallet", lockunspent, },
{ "wallet", rescanblockchain, },
{ "wallet", send, },
{ "wallet", sendmany, },
{ "wallet", sendtoaddress, },
{ "wallet", sethdseed, },
{ "wallet", setlabel, },
{ "wallet", settxfee, },
{ "wallet", setwalletflag, },
{ "wallet", signmessage, },
{ "wallet", signrawtransactionwithwallet, },
{ "wallet", unloadwallet, },
{ "wallet", upgradewallet, },
{ "wallet", walletcreatefundedpsbt, },
{ "wallet", walletprocesspsbt, },
// For testing purpose
{ "hidden", createwallettransaction, },
};
// clang-format on
return commands;
}
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index 7eb1a8fe1..101c34808 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -1,1093 +1,1085 @@
// Copyright (c) 2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <wallet/spend.h>
#include <common/args.h>
#include <common/system.h>
#include <consensus/validation.h>
#include <interfaces/chain.h>
#include <policy/policy.h>
#include <util/check.h>
#include <util/insert.h>
#include <util/moneystr.h>
#include <util/translation.h>
#include <wallet/coincontrol.h>
#include <wallet/fees.h>
#include <wallet/receive.h>
#include <wallet/transaction.h>
#include <wallet/wallet.h>
using interfaces::FoundBlock;
static const size_t OUTPUT_GROUP_MAX_ENTRIES = 10;
int GetTxSpendSize(const CWallet &wallet, const CWalletTx &wtx,
unsigned int out, bool use_max_sig) {
return CalculateMaximumSignedInputSize(wtx.tx->vout[out], &wallet,
use_max_sig);
}
std::string COutput::ToString() const {
return strprintf("COutput(%s, %d, %d) [%s]", tx->GetId().ToString(), i,
nDepth, FormatMoney(tx->tx->vout[i].nValue));
}
int CalculateMaximumSignedInputSize(const CTxOut &txout, const CWallet *wallet,
bool use_max_sig) {
CMutableTransaction txn;
txn.vin.push_back(CTxIn(COutPoint()));
if (!wallet->DummySignInput(txn.vin[0], txout, use_max_sig)) {
return -1;
}
return GetSerializeSize(txn.vin[0], PROTOCOL_VERSION);
}
// txouts needs to be in the order of tx.vin
int64_t CalculateMaximumSignedTxSize(const CTransaction &tx,
const CWallet *wallet,
const std::vector<CTxOut> &txouts,
bool use_max_sig) {
CMutableTransaction txNew(tx);
if (!wallet->DummySignTx(txNew, txouts, use_max_sig)) {
return -1;
}
return GetSerializeSize(txNew, PROTOCOL_VERSION);
}
int64_t CalculateMaximumSignedTxSize(const CTransaction &tx,
const CWallet *wallet, bool use_max_sig) {
std::vector<CTxOut> txouts;
for (auto &input : tx.vin) {
const auto mi = wallet->mapWallet.find(input.prevout.GetTxId());
// Can not estimate size without knowing the input details
if (mi == wallet->mapWallet.end()) {
return -1;
}
assert(input.prevout.GetN() < mi->second.tx->vout.size());
txouts.emplace_back(mi->second.tx->vout[input.prevout.GetN()]);
}
return CalculateMaximumSignedTxSize(tx, wallet, txouts, use_max_sig);
}
void AvailableCoins(const CWallet &wallet, std::vector<COutput> &vCoins,
const CCoinControl *coinControl,
const Amount nMinimumAmount, const Amount nMaximumAmount,
const Amount nMinimumSumAmount,
const uint64_t nMaximumCount) {
AssertLockHeld(wallet.cs_wallet);
vCoins.clear();
Amount nTotal = Amount::zero();
// Either the WALLET_FLAG_AVOID_REUSE flag is not set (in which case we
// always allow), or we default to avoiding, and only in the case where a
// coin control object is provided, and has the avoid address reuse flag set
// to false, do we allow already used addresses
bool allow_used_addresses =
!wallet.IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE) ||
(coinControl && !coinControl->m_avoid_address_reuse);
const int min_depth = {coinControl ? coinControl->m_min_depth
: DEFAULT_MIN_DEPTH};
const int max_depth = {coinControl ? coinControl->m_max_depth
: DEFAULT_MAX_DEPTH};
const bool only_safe = {coinControl ? !coinControl->m_include_unsafe_inputs
: true};
std::set<TxId> trusted_parents;
for (const auto &entry : wallet.mapWallet) {
const TxId &wtxid = entry.first;
const CWalletTx &wtx = entry.second;
if (wallet.IsTxImmatureCoinBase(wtx)) {
continue;
}
int nDepth = wallet.GetTxDepthInMainChain(wtx);
if (nDepth < 0) {
continue;
}
// We should not consider coins which aren't at least in our mempool.
// It's possible for these to be conflicted via ancestors which we may
// never be able to detect.
if (nDepth == 0 && !wtx.InMempool()) {
continue;
}
bool safeTx = CachedTxIsTrusted(wallet, wtx, trusted_parents);
// Bitcoin-ABC: Removed check that prevents consideration of coins from
// transactions that are replacing other transactions. This check based
// on wtx.mapValue.count("replaces_txid") which was not being set
// anywhere.
// Similarly, we should not consider coins from transactions that have
// been replaced. In the example above, we would want to prevent
// creation of a transaction A' spending an output of A, because if
// transaction B were initially confirmed, conflicting with A and A', we
// wouldn't want to the user to create a transaction D intending to
// replace A', but potentially resulting in a scenario where A, A', and
// D could all be accepted (instead of just B and D, or just A and A'
// like the user would want).
// Bitcoin-ABC: retained this check as 'replaced_by_txid' is still set
// in the wallet code.
if (nDepth == 0 && wtx.mapValue.count("replaced_by_txid")) {
safeTx = false;
}
if (only_safe && !safeTx) {
continue;
}
if (nDepth < min_depth || nDepth > max_depth) {
continue;
}
for (uint32_t i = 0; i < wtx.tx->vout.size(); i++) {
// Only consider selected coins if add_inputs is false
if (coinControl && !coinControl->m_add_inputs &&
!coinControl->IsSelected(COutPoint(entry.first, i))) {
continue;
}
if (wtx.tx->vout[i].nValue < nMinimumAmount ||
wtx.tx->vout[i].nValue > nMaximumAmount) {
continue;
}
const COutPoint outpoint(wtxid, i);
if (coinControl && coinControl->HasSelected() &&
!coinControl->fAllowOtherInputs &&
!coinControl->IsSelected(outpoint)) {
continue;
}
if (wallet.IsLockedCoin(outpoint)) {
continue;
}
if (wallet.IsSpent(outpoint)) {
continue;
}
isminetype mine = wallet.IsMine(wtx.tx->vout[i]);
if (mine == ISMINE_NO) {
continue;
}
if (!allow_used_addresses && wallet.IsSpentKey(wtxid, i)) {
continue;
}
std::unique_ptr<SigningProvider> provider =
wallet.GetSolvingProvider(wtx.tx->vout[i].scriptPubKey);
bool solvable =
provider ? IsSolvable(*provider, wtx.tx->vout[i].scriptPubKey)
: false;
bool spendable =
((mine & ISMINE_SPENDABLE) != ISMINE_NO) ||
(((mine & ISMINE_WATCH_ONLY) != ISMINE_NO) &&
(coinControl && coinControl->fAllowWatchOnly && solvable));
vCoins.push_back(
COutput(wallet, wtx, i, nDepth, spendable, solvable, safeTx,
(coinControl && coinControl->fAllowWatchOnly)));
// Checks the sum amount of all UTXO's.
if (nMinimumSumAmount != MAX_MONEY) {
nTotal += wtx.tx->vout[i].nValue;
if (nTotal >= nMinimumSumAmount) {
return;
}
}
// Checks the maximum number of UTXO's.
if (nMaximumCount > 0 && vCoins.size() >= nMaximumCount) {
return;
}
}
}
}
Amount GetAvailableBalance(const CWallet &wallet,
const CCoinControl *coinControl) {
LOCK(wallet.cs_wallet);
Amount balance = Amount::zero();
std::vector<COutput> vCoins;
AvailableCoins(wallet, vCoins, coinControl);
for (const COutput &out : vCoins) {
if (out.fSpendable) {
balance += out.tx->tx->vout[out.i].nValue;
}
}
return balance;
}
const CTxOut &FindNonChangeParentOutput(const CWallet &wallet,
const CTransaction &tx, int output) {
AssertLockHeld(wallet.cs_wallet);
const CTransaction *ptx = &tx;
int n = output;
while (OutputIsChange(wallet, ptx->vout[n]) && ptx->vin.size() > 0) {
const COutPoint &prevout = ptx->vin[0].prevout;
auto it = wallet.mapWallet.find(prevout.GetTxId());
if (it == wallet.mapWallet.end() ||
it->second.tx->vout.size() <= prevout.GetN() ||
!wallet.IsMine(it->second.tx->vout[prevout.GetN()])) {
break;
}
ptx = it->second.tx.get();
n = prevout.GetN();
}
return ptx->vout[n];
}
std::map<CTxDestination, std::vector<COutput>>
ListCoins(const CWallet &wallet) {
AssertLockHeld(wallet.cs_wallet);
std::map<CTxDestination, std::vector<COutput>> result;
std::vector<COutput> availableCoins;
AvailableCoins(wallet, availableCoins);
for (COutput &coin : availableCoins) {
CTxDestination address;
if ((coin.fSpendable ||
(wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) &&
coin.fSolvable)) &&
ExtractDestination(
FindNonChangeParentOutput(wallet, *coin.tx->tx, coin.i)
.scriptPubKey,
address)) {
result[address].emplace_back(std::move(coin));
}
}
std::vector<COutPoint> lockedCoins;
wallet.ListLockedCoins(lockedCoins);
// Include watch-only for LegacyScriptPubKeyMan wallets without private keys
const bool include_watch_only =
wallet.GetLegacyScriptPubKeyMan() &&
wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
const isminetype is_mine_filter =
include_watch_only ? ISMINE_WATCH_ONLY : ISMINE_SPENDABLE;
for (const auto &output : lockedCoins) {
auto it = wallet.mapWallet.find(output.GetTxId());
if (it != wallet.mapWallet.end()) {
int depth = wallet.GetTxDepthInMainChain(it->second);
if (depth >= 0 && output.GetN() < it->second.tx->vout.size() &&
wallet.IsMine(it->second.tx->vout[output.GetN()]) ==
is_mine_filter) {
CTxDestination address;
if (ExtractDestination(FindNonChangeParentOutput(wallet,
*it->second.tx,
output.GetN())
.scriptPubKey,
address)) {
result[address].emplace_back(
wallet, it->second, output.GetN(), depth,
true /* spendable */, true /* solvable */,
false /* safe */);
}
}
}
}
return result;
}
std::vector<OutputGroup>
GroupOutputs(const CWallet &wallet, const std::vector<COutput> &outputs,
bool separate_coins, const CFeeRate &effective_feerate,
const CFeeRate &long_term_feerate,
const CoinEligibilityFilter &filter, bool positive_only) {
std::vector<OutputGroup> groups_out;
if (separate_coins) {
// Single coin means no grouping. Each COutput gets its own OutputGroup.
for (const COutput &output : outputs) {
// Skip outputs we cannot spend
if (!output.fSpendable) {
continue;
}
CInputCoin input_coin = output.GetInputCoin();
// Make an OutputGroup containing just this output
OutputGroup group{effective_feerate, long_term_feerate};
group.Insert(input_coin, output.nDepth,
CachedTxIsFromMe(wallet, *output.tx, ISMINE_ALL),
positive_only);
// Check the OutputGroup's eligibility. Only add the eligible ones.
if (positive_only && group.effective_value <= Amount::zero()) {
continue;
}
if (group.m_outputs.size() > 0 &&
group.EligibleForSpending(filter)) {
groups_out.push_back(group);
}
}
return groups_out;
}
// We want to combine COutputs that have the same scriptPubKey into single
// OutputGroups except when there are more than OUTPUT_GROUP_MAX_ENTRIES
// COutputs grouped in an OutputGroup.
// To do this, we maintain a map where the key is the scriptPubKey and the
// value is a vector of OutputGroups.
// For each COutput, we check if the scriptPubKey is in the map, and if it
// is, the COutput's CInputCoin is added to the last OutputGroup in the
// vector for the scriptPubKey. When the last OutputGroup has
// OUTPUT_GROUP_MAX_ENTRIES CInputCoins, a new OutputGroup is added to the
// end of the vector.
std::map<CScript, std::vector<OutputGroup>> spk_to_groups_map;
for (const auto &output : outputs) {
// Skip outputs we cannot spend
if (!output.fSpendable) {
continue;
}
CInputCoin input_coin = output.GetInputCoin();
CScript spk = input_coin.txout.scriptPubKey;
std::vector<OutputGroup> &groups = spk_to_groups_map[spk];
if (groups.size() == 0) {
// No OutputGroups for this scriptPubKey yet, add one
groups.emplace_back(effective_feerate, long_term_feerate);
}
// Get the last OutputGroup in the vector so that we can add the
// CInputCoin to it.
// A pointer is used here so that group can be reassigned later if it
// is full.
OutputGroup *group = &groups.back();
// Check if this OutputGroup is full. We limit to
// OUTPUT_GROUP_MAX_ENTRIES when using -avoidpartialspends to avoid
// surprising users with very high fees.
if (group->m_outputs.size() >= OUTPUT_GROUP_MAX_ENTRIES) {
// The last output group is full, add a new group to the vector and
// use that group for the insertion
groups.emplace_back(effective_feerate, long_term_feerate);
group = &groups.back();
}
// Add the input_coin to group
group->Insert(input_coin, output.nDepth,
CachedTxIsFromMe(wallet, *output.tx, ISMINE_ALL),
positive_only);
}
// Now we go through the entire map and pull out the OutputGroups
for (const auto &spk_and_groups_pair : spk_to_groups_map) {
const std::vector<OutputGroup> &groups_per_spk =
spk_and_groups_pair.second;
// Go through the vector backwards. This allows for the first item we
// deal with being the partial group.
for (auto group_it = groups_per_spk.rbegin();
group_it != groups_per_spk.rend(); group_it++) {
const OutputGroup &group = *group_it;
// Don't include partial groups if there are full groups too and we
// don't want partial groups
if (group_it == groups_per_spk.rbegin() &&
groups_per_spk.size() > 1 && !filter.m_include_partial_groups) {
continue;
}
// Check the OutputGroup's eligibility. Only add the eligible ones.
if (positive_only && group.effective_value <= Amount::zero()) {
continue;
}
if (group.m_outputs.size() > 0 &&
group.EligibleForSpending(filter)) {
groups_out.push_back(group);
}
}
}
return groups_out;
}
bool SelectCoinsMinConf(const CWallet &wallet, const Amount nTargetValue,
const CoinEligibilityFilter &eligibility_filter,
std::vector<COutput> coins,
std::set<CInputCoin> &setCoinsRet, Amount &nValueRet,
const CoinSelectionParams &coin_selection_params,
bool &bnb_used) {
setCoinsRet.clear();
nValueRet = Amount::zero();
if (coin_selection_params.use_bnb) {
// Get long term estimate
CCoinControl temp;
temp.m_confirm_target = 1008;
CFeeRate long_term_feerate = GetMinimumFeeRate(wallet, temp);
// Get the feerate for effective value.
// When subtracting the fee from the outputs, we want the effective
// feerate to be 0
CFeeRate effective_feerate{Amount::zero()};
if (!coin_selection_params.m_subtract_fee_outputs) {
effective_feerate = coin_selection_params.effective_fee;
}
std::vector<OutputGroup> groups = GroupOutputs(
wallet, coins, !coin_selection_params.m_avoid_partial_spends,
effective_feerate, long_term_feerate, eligibility_filter,
/*positive_only=*/true);
// Calculate cost of change
Amount cost_of_change = wallet.chain().relayDustFee().GetFee(
coin_selection_params.change_spend_size) +
coin_selection_params.effective_fee.GetFee(
coin_selection_params.change_output_size);
// Calculate the fees for things that aren't inputs
Amount not_input_fees = coin_selection_params.effective_fee.GetFee(
coin_selection_params.tx_noinputs_size);
bnb_used = true;
return SelectCoinsBnB(groups, nTargetValue, cost_of_change, setCoinsRet,
nValueRet, not_input_fees);
} else {
std::vector<OutputGroup> groups = GroupOutputs(
wallet, coins, !coin_selection_params.m_avoid_partial_spends,
CFeeRate(Amount::zero()), CFeeRate(Amount::zero()),
eligibility_filter,
/*positive_only=*/false);
bnb_used = false;
return KnapsackSolver(nTargetValue, groups, setCoinsRet, nValueRet);
}
}
bool SelectCoins(const CWallet &wallet,
const std::vector<COutput> &vAvailableCoins,
const Amount nTargetValue, std::set<CInputCoin> &setCoinsRet,
Amount &nValueRet, const CCoinControl &coin_control,
CoinSelectionParams &coin_selection_params, bool &bnb_used) {
std::vector<COutput> vCoins(vAvailableCoins);
Amount value_to_select = nTargetValue;
// Default to bnb was not used. If we use it, we set it later
bnb_used = false;
// coin control -> return all selected outputs (we want all selected to go
// into the transaction for sure)
if (coin_control.HasSelected() && !coin_control.fAllowOtherInputs) {
for (const COutput &out : vCoins) {
if (!out.fSpendable) {
continue;
}
nValueRet += out.tx->tx->vout[out.i].nValue;
setCoinsRet.insert(out.GetInputCoin());
}
return (nValueRet >= nTargetValue);
}
// Calculate value from preset inputs and store them.
std::set<CInputCoin> setPresetCoins;
Amount nValueFromPresetInputs = Amount::zero();
std::vector<COutPoint> vPresetInputs;
coin_control.ListSelected(vPresetInputs);
for (const COutPoint &outpoint : vPresetInputs) {
std::map<TxId, CWalletTx>::const_iterator it =
wallet.mapWallet.find(outpoint.GetTxId());
if (it != wallet.mapWallet.end()) {
const CWalletTx &wtx = it->second;
// Clearly invalid input, fail
if (wtx.tx->vout.size() <= outpoint.GetN()) {
return false;
}
// Just to calculate the marginal byte size
CInputCoin coin(
wtx.tx, outpoint.GetN(),
GetTxSpendSize(wallet, wtx, outpoint.GetN(), false));
nValueFromPresetInputs += coin.txout.nValue;
if (coin.m_input_bytes <= 0) {
// Not solvable, can't estimate size for fee
return false;
}
coin.effective_value =
coin.txout.nValue -
coin_selection_params.effective_fee.GetFee(coin.m_input_bytes);
if (coin_selection_params.use_bnb) {
value_to_select -= coin.effective_value;
} else {
value_to_select -= coin.txout.nValue;
}
setPresetCoins.insert(coin);
} else {
return false; // TODO: Allow non-wallet inputs
}
}
// Remove preset inputs from vCoins
for (std::vector<COutput>::iterator it = vCoins.begin();
it != vCoins.end() && coin_control.HasSelected();) {
if (setPresetCoins.count(it->GetInputCoin())) {
it = vCoins.erase(it);
} else {
++it;
}
}
// form groups from remaining coins; note that preset coins will not
// automatically have their associated (same address) coins included
if (coin_control.m_avoid_partial_spends &&
vCoins.size() > OUTPUT_GROUP_MAX_ENTRIES) {
// Cases where we have 11+ outputs all pointing to the same destination
// may result in privacy leaks as they will potentially be
// deterministically sorted. We solve that by explicitly shuffling the
// outputs before processing
Shuffle(vCoins.begin(), vCoins.end(), FastRandomContext());
}
bool res =
value_to_select <= Amount::zero() ||
SelectCoinsMinConf(wallet, value_to_select, CoinEligibilityFilter(1, 6),
vCoins, setCoinsRet, nValueRet,
coin_selection_params, bnb_used) ||
SelectCoinsMinConf(wallet, value_to_select, CoinEligibilityFilter(1, 1),
vCoins, setCoinsRet, nValueRet,
coin_selection_params, bnb_used) ||
(wallet.m_spend_zero_conf_change &&
SelectCoinsMinConf(wallet, value_to_select,
CoinEligibilityFilter(0, 1), vCoins, setCoinsRet,
nValueRet, coin_selection_params, bnb_used)) ||
(wallet.m_spend_zero_conf_change &&
SelectCoinsMinConf(wallet, value_to_select,
CoinEligibilityFilter(0, 1), vCoins, setCoinsRet,
nValueRet, coin_selection_params, bnb_used)) ||
(wallet.m_spend_zero_conf_change &&
SelectCoinsMinConf(wallet, value_to_select,
CoinEligibilityFilter(0, 1), vCoins, setCoinsRet,
nValueRet, coin_selection_params, bnb_used)) ||
(wallet.m_spend_zero_conf_change &&
SelectCoinsMinConf(
wallet, value_to_select,
CoinEligibilityFilter(0, 1, /*include_partial_groups=*/true),
vCoins, setCoinsRet, nValueRet, coin_selection_params,
bnb_used)) ||
(wallet.m_spend_zero_conf_change &&
SelectCoinsMinConf(
wallet, value_to_select,
CoinEligibilityFilter(0, 1, /*include_partial_groups=*/true),
vCoins, setCoinsRet, nValueRet, coin_selection_params,
bnb_used)) ||
// Try with unsafe inputs if they are allowed. This may spend
// unconfirmed outputs received from other wallets.
(coin_control.m_include_unsafe_inputs &&
SelectCoinsMinConf(
wallet, value_to_select,
CoinEligibilityFilter(/*conf_mine=*/0, /*conf_theirs=*/0,
/*include_partial_groups=*/true),
vCoins, setCoinsRet, nValueRet, coin_selection_params, bnb_used));
// Because SelectCoinsMinConf clears the setCoinsRet, we now add the
// possible inputs to the coinset.
util::insert(setCoinsRet, setPresetCoins);
// Add preset inputs to the total value selected.
nValueRet += nValueFromPresetInputs;
return res;
}
-static std::optional<CreatedTransactionResult> CreateTransactionInternal(
+static util::Result<CreatedTransactionResult> CreateTransactionInternal(
CWallet &wallet, const std::vector<CRecipient> &vecSend, int change_pos,
- bilingual_str &error, const CCoinControl &coin_control, bool sign) {
+ const CCoinControl &coin_control, bool sign) {
// TODO: remember to add the lock annotation when adding AssertLockHeld.
// The lock annotation was added by core in PR22100, but due to
// other missing backports it was not possible to add it during that
// backport.
// out variables, to be packed into returned result structure
- CTransactionRef tx;
Amount nFeeRet;
int nChangePosInOut = change_pos;
Amount nValue = Amount::zero();
const OutputType change_type = wallet.TransactionChangeType(
coin_control.m_change_type ? *coin_control.m_change_type
: wallet.m_default_change_type,
vecSend);
ReserveDestination reservedest(&wallet, change_type);
int nChangePosRequest = nChangePosInOut;
unsigned int nSubtractFeeFromAmount = 0;
for (const auto &recipient : vecSend) {
if (nValue < Amount::zero() || recipient.nAmount < Amount::zero()) {
- error = _("Transaction amounts must not be negative");
- return std::nullopt;
+ return util::Error{_("Transaction amounts must not be negative")};
}
nValue += recipient.nAmount;
if (recipient.fSubtractFeeFromAmount) {
nSubtractFeeFromAmount++;
}
}
if (vecSend.empty()) {
- error = _("Transaction must have at least one recipient");
- return std::nullopt;
+ return util::Error{_("Transaction must have at least one recipient")};
}
- CMutableTransaction txNew;
-
+ CTransactionRef tx;
{
+ CMutableTransaction txNew;
std::set<CInputCoin> setCoins;
LOCK(wallet.cs_wallet);
// Previously the locktime was set to the current block height, to
// prevent fee-sniping. This is now disabled as fee-sniping is mitigated
// by avalanche post-consensus. Consistently Using a locktime of 0 for
// most wallets in the ecosystem improves privacy, as this is the
// easiest solution to implement for light wallets which are not aware
// of the current block height.
txNew.nLockTime = 0;
std::vector<COutput> vAvailableCoins;
AvailableCoins(wallet, vAvailableCoins, &coin_control);
// Parameters for coin selection, init with dummy
CoinSelectionParams coin_selection_params;
coin_selection_params.m_avoid_partial_spends =
coin_control.m_avoid_partial_spends;
// Create change script that will be used if we need change
// TODO: pass in scriptChange instead of reservedest so
// change transaction isn't always pay-to-bitcoin-address
CScript scriptChange;
+ bilingual_str error; // possible error str
// coin control: send change to custom address
if (!std::get_if<CNoDestination>(&coin_control.destChange)) {
scriptChange = GetScriptForDestination(coin_control.destChange);
// no coin control: send change to newly generated address
} else {
// Note: We use a new key here to keep it from being obvious
// which side is the change.
// The drawback is that by not reusing a previous key, the
// change may be lost if a backup is restored, if the backup
// doesn't have the new private key for the change. If we
// reused the old key, it would be possible to add code to look
// for and rediscover unknown transactions that were written
// with keys of ours to recover post-backup change.
// Reserve a new key pair from key pool. If it fails, provide a
// dummy destination in case we don't need change.
CTxDestination dest;
if (!reservedest.GetReservedDestination(dest, true)) {
error = _("Transaction needs a change address, but we can't "
"generate it. Please call keypoolrefill first.");
}
scriptChange = GetScriptForDestination(dest);
// A valid destination implies a change script (and
// vice-versa). An empty change script will abort later, if the
// change keypool ran out, but change is required.
CHECK_NONFATAL(IsValidDestination(dest) != scriptChange.empty());
}
CTxOut change_prototype_txout(Amount::zero(), scriptChange);
coin_selection_params.change_output_size =
GetSerializeSize(change_prototype_txout);
// Get the fee rate to use effective values in coin selection
CFeeRate nFeeRateNeeded = GetMinimumFeeRate(wallet, coin_control);
// Do not, ever, assume that it's fine to change the fee rate if the
// user has explicitly provided one
if (coin_control.m_feerate &&
nFeeRateNeeded > *coin_control.m_feerate) {
- error = strprintf(_("Fee rate (%s) is lower than the minimum fee "
- "rate setting (%s)"),
- coin_control.m_feerate->ToString(),
- nFeeRateNeeded.ToString());
- return std::nullopt;
+ return util::Error{strprintf(
+ _("Fee rate (%s) is lower than the minimum fee "
+ "rate setting (%s)"),
+ coin_control.m_feerate->ToString(), nFeeRateNeeded.ToString())};
}
nFeeRet = Amount::zero();
bool pick_new_inputs = true;
Amount nValueIn = Amount::zero();
// BnB selector is the only selector used when this is true.
// That should only happen on the first pass through the loop.
coin_selection_params.use_bnb = true;
// If we are doing subtract fee from recipient, don't use effective
// values
coin_selection_params.m_subtract_fee_outputs =
nSubtractFeeFromAmount != 0;
// Start with no fee and loop until there is enough fee
while (true) {
nChangePosInOut = nChangePosRequest;
txNew.vin.clear();
txNew.vout.clear();
bool fFirst = true;
Amount nValueToSelect = nValue;
if (nSubtractFeeFromAmount == 0) {
nValueToSelect += nFeeRet;
}
// vouts to the payees
if (!coin_selection_params.m_subtract_fee_outputs) {
// Static size overhead + outputs vsize. 4 nVersion, 4
// nLocktime, 1 input count, 1 output count
coin_selection_params.tx_noinputs_size = 10;
}
// vouts to the payees
for (const auto &recipient : vecSend) {
CTxOut txout(recipient.nAmount, recipient.scriptPubKey);
if (recipient.fSubtractFeeFromAmount) {
assert(nSubtractFeeFromAmount != 0);
// Subtract fee equally from each selected recipient.
txout.nValue -= nFeeRet / int(nSubtractFeeFromAmount);
// First receiver pays the remainder not divisible by output
// count.
if (fFirst) {
fFirst = false;
txout.nValue -= nFeeRet % int(nSubtractFeeFromAmount);
}
}
// Include the fee cost for outputs. Note this is only used for
// BnB right now
if (!coin_selection_params.m_subtract_fee_outputs) {
coin_selection_params.tx_noinputs_size +=
::GetSerializeSize(txout, PROTOCOL_VERSION);
}
if (IsDust(txout, wallet.chain().relayDustFee())) {
if (recipient.fSubtractFeeFromAmount &&
nFeeRet > Amount::zero()) {
if (txout.nValue < Amount::zero()) {
error = _("The transaction amount is too small to "
"pay the fee");
} else {
error = _("The transaction amount is too small to "
"send after the fee has been deducted");
}
} else {
error = _("Transaction amount too small");
}
- return std::nullopt;
+ return util::Error{error};
}
txNew.vout.push_back(txout);
}
// Choose coins to use
bool bnb_used = false;
if (pick_new_inputs) {
nValueIn = Amount::zero();
setCoins.clear();
int change_spend_size = CalculateMaximumSignedInputSize(
change_prototype_txout, &wallet);
// If the wallet doesn't know how to sign change output, assume
// p2pkh as lower-bound to allow BnB to do it's thing
if (change_spend_size == -1) {
coin_selection_params.change_spend_size =
DUMMY_P2PKH_INPUT_SIZE;
} else {
coin_selection_params.change_spend_size =
size_t(change_spend_size);
}
coin_selection_params.effective_fee = nFeeRateNeeded;
if (!SelectCoins(wallet, vAvailableCoins, nValueToSelect,
setCoins, nValueIn, coin_control,
coin_selection_params, bnb_used)) {
// If BnB was used, it was the first pass. No longer the
// first pass and continue loop with knapsack.
if (bnb_used) {
coin_selection_params.use_bnb = false;
continue;
} else {
- error = _("Insufficient funds");
- return std::nullopt;
+ return util::Error{_("Insufficient funds")};
}
}
} else {
bnb_used = false;
}
const Amount nChange = nValueIn - nValueToSelect;
if (nChange > Amount::zero()) {
// Fill a vout to ourself.
CTxOut newTxOut(nChange, scriptChange);
// Never create dust outputs; if we would, just add the dust to
// the fee.
// The nChange when BnB is used is always going to go to fees.
if (IsDust(newTxOut, wallet.chain().relayDustFee()) ||
bnb_used) {
nChangePosInOut = -1;
nFeeRet += nChange;
} else {
if (nChangePosInOut == -1) {
// Insert change txn at random position:
nChangePosInOut = GetRand<int>(txNew.vout.size() + 1);
} else if ((unsigned int)nChangePosInOut >
txNew.vout.size()) {
- error = _("Change index out of range");
- return std::nullopt;
+ return util::Error{_("Change index out of range")};
}
std::vector<CTxOut>::iterator position =
txNew.vout.begin() + nChangePosInOut;
txNew.vout.insert(position, newTxOut);
}
} else {
nChangePosInOut = -1;
}
// Dummy fill vin for maximum size estimation
//
for (const auto &coin : setCoins) {
txNew.vin.push_back(CTxIn(coin.outpoint, CScript()));
}
CTransaction txNewConst(txNew);
int nBytes = CalculateMaximumSignedTxSize(
txNewConst, &wallet, coin_control.fAllowWatchOnly);
if (nBytes < 0) {
- error = _("Signing transaction failed");
- return std::nullopt;
+ return util::Error{_("Signing transaction failed")};
}
Amount nFeeNeeded = GetMinimumFee(wallet, nBytes, coin_control);
if (nFeeRet >= nFeeNeeded) {
// Reduce fee to only the needed amount if possible. This
// prevents potential overpayment in fees if the coins selected
// to meet nFeeNeeded result in a transaction that requires less
// fee than the prior iteration.
// If we have no change and a big enough excess fee, then try to
// construct transaction again only without picking new inputs.
// We now know we only need the smaller fee (because of reduced
// tx size) and so we should add a change output. Only try this
// once.
if (nChangePosInOut == -1 && nSubtractFeeFromAmount == 0 &&
pick_new_inputs) {
// Add 2 as a buffer in case increasing # of outputs changes
// compact size
unsigned int tx_size_with_change =
nBytes + coin_selection_params.change_output_size + 2;
Amount fee_needed_with_change = GetMinimumFee(
wallet, tx_size_with_change, coin_control);
Amount minimum_value_for_change = GetDustThreshold(
change_prototype_txout, wallet.chain().relayDustFee());
if (nFeeRet >=
fee_needed_with_change + minimum_value_for_change) {
pick_new_inputs = false;
nFeeRet = fee_needed_with_change;
continue;
}
}
// If we have change output already, just increase it
if (nFeeRet > nFeeNeeded && nChangePosInOut != -1 &&
nSubtractFeeFromAmount == 0) {
Amount extraFeePaid = nFeeRet - nFeeNeeded;
std::vector<CTxOut>::iterator change_position =
txNew.vout.begin() + nChangePosInOut;
change_position->nValue += extraFeePaid;
nFeeRet -= extraFeePaid;
}
// Done, enough fee included.
break;
} else if (!pick_new_inputs) {
// This shouldn't happen, we should have had enough excess fee
// to pay for the new output and still meet nFeeNeeded.
// Or we should have just subtracted fee from recipients and
// nFeeNeeded should not have changed.
- error = _("Transaction fee and change calculation failed");
- return std::nullopt;
+ return util::Error{
+ _("Transaction fee and change calculation failed")};
}
// Try to reduce change to include necessary fee.
if (nChangePosInOut != -1 && nSubtractFeeFromAmount == 0) {
Amount additionalFeeNeeded = nFeeNeeded - nFeeRet;
std::vector<CTxOut>::iterator change_position =
txNew.vout.begin() + nChangePosInOut;
// Only reduce change if remaining amount is still a large
// enough output.
if (change_position->nValue >=
MIN_FINAL_CHANGE + additionalFeeNeeded) {
change_position->nValue -= additionalFeeNeeded;
nFeeRet += additionalFeeNeeded;
// Done, able to increase fee from change.
break;
}
}
// If subtracting fee from recipients, we now know what fee we
// need to subtract, we have no reason to reselect inputs.
if (nSubtractFeeFromAmount > 0) {
pick_new_inputs = false;
}
// Include more fee and try again.
nFeeRet = nFeeNeeded;
coin_selection_params.use_bnb = false;
continue;
}
// Give up if change keypool ran out and change is required
if (scriptChange.empty() && nChangePosInOut != -1) {
- return std::nullopt;
+ return util::Error{error};
}
// Shuffle selected coins and fill in final vin
txNew.vin.clear();
std::vector<CInputCoin> selected_coins(setCoins.begin(),
setCoins.end());
Shuffle(selected_coins.begin(), selected_coins.end(),
FastRandomContext());
// Note how the sequence number is set to non-maxint so that
// the nLockTime set above actually works.
for (const auto &coin : selected_coins) {
txNew.vin.push_back(
CTxIn(coin.outpoint, CScript(),
std::numeric_limits<uint32_t>::max() - 1));
}
if (sign && !wallet.SignTransaction(txNew)) {
- error = _("Signing transaction failed");
- return std::nullopt;
+ return util::Error{_("Signing transaction failed")};
}
// Return the constructed transaction data.
tx = MakeTransactionRef(std::move(txNew));
// Limit size.
if (tx->GetTotalSize() > MAX_STANDARD_TX_SIZE) {
- error = _("Transaction too large");
- return std::nullopt;
+ return util::Error{_("Transaction too large")};
}
}
if (nFeeRet > wallet.m_default_max_tx_fee) {
- error = TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED);
- return std::nullopt;
+ return util::Error{
+ TransactionErrorString(TransactionError::MAX_FEE_EXCEEDED)};
}
// Before we return success, we assume any change key will be used to
// prevent accidental re-use.
reservedest.KeepDestination();
return CreatedTransactionResult(tx, nFeeRet, nChangePosInOut);
}
-std::optional<CreatedTransactionResult>
+util::Result<CreatedTransactionResult>
CreateTransaction(CWallet &wallet, const std::vector<CRecipient> &vecSend,
- int change_pos, bilingual_str &error,
- const CCoinControl &coin_control, bool sign) {
- std::optional<CreatedTransactionResult> txr_ungrouped =
- CreateTransactionInternal(wallet, vecSend, change_pos, error,
- coin_control, sign);
- if (!txr_ungrouped) {
- return std::nullopt;
+ int change_pos, const CCoinControl &coin_control, bool sign) {
+ auto res = CreateTransactionInternal(wallet, vecSend, change_pos,
+ coin_control, sign);
+ if (!res) {
+ return res;
}
+ const auto &txr_ungrouped = *res;
// try with avoidpartialspends unless it's enabled already
// 0 as fee means non-functional fee rate estimation
- if (txr_ungrouped->fee > Amount::zero() &&
+ if (txr_ungrouped.fee > Amount::zero() &&
wallet.m_max_aps_fee > (-1 * SATOSHI) &&
!coin_control.m_avoid_partial_spends) {
CCoinControl tmp_cc = coin_control;
tmp_cc.m_avoid_partial_spends = true;
// fired and forgotten; if an error occurs, we discard the results
bilingual_str error2;
- std::optional<CreatedTransactionResult> txr_grouped =
- CreateTransactionInternal(wallet, vecSend, change_pos, error2,
- tmp_cc, sign);
+ auto txr_grouped = CreateTransactionInternal(wallet, vecSend,
+ change_pos, tmp_cc, sign);
if (txr_grouped) {
// if fee of this alternative one is within the range of the max
// fee, we use this one
const bool use_aps =
- txr_grouped->fee <= txr_ungrouped->fee + wallet.m_max_aps_fee;
+ txr_grouped->fee <= txr_ungrouped.fee + wallet.m_max_aps_fee;
wallet.WalletLogPrintf(
"Fee non-grouped = %lld, grouped = %lld, using %s\n",
- txr_ungrouped->fee, txr_grouped->fee,
+ txr_ungrouped.fee, txr_grouped->fee,
use_aps ? "grouped" : "non-grouped");
if (use_aps) {
return txr_grouped;
}
}
}
- return txr_ungrouped;
+ return res;
}
bool FundTransaction(CWallet &wallet, CMutableTransaction &tx, Amount &nFeeRet,
int &nChangePosInOut, bilingual_str &error,
bool lockUnspents,
const std::set<int> &setSubtractFeeFromOutputs,
CCoinControl coinControl) {
std::vector<CRecipient> vecSend;
// Turn the txout set into a CRecipient vector.
for (size_t idx = 0; idx < tx.vout.size(); idx++) {
const CTxOut &txOut = tx.vout[idx];
CRecipient recipient = {txOut.scriptPubKey, txOut.nValue,
setSubtractFeeFromOutputs.count(idx) == 1};
vecSend.push_back(recipient);
}
coinControl.fAllowOtherInputs = true;
for (const CTxIn &txin : tx.vin) {
coinControl.Select(txin.prevout);
}
// Acquire the locks to prevent races to the new locked unspents between the
// CreateTransaction call and LockCoin calls (when lockUnspents is true).
LOCK(wallet.cs_wallet);
- std::optional<CreatedTransactionResult> txr = CreateTransaction(
- wallet, vecSend, nChangePosInOut, error, coinControl, false);
- if (!txr) {
+ auto res =
+ CreateTransaction(wallet, vecSend, nChangePosInOut, coinControl, false);
+ if (!res) {
+ error = util::ErrorString(res);
return false;
}
- CTransactionRef tx_new = txr->tx;
- nFeeRet = txr->fee;
- nChangePosInOut = txr->change_pos;
+ const auto &txr = *res;
+ CTransactionRef tx_new = txr.tx;
+ nFeeRet = txr.fee;
+ nChangePosInOut = txr.change_pos;
if (nChangePosInOut != -1) {
tx.vout.insert(tx.vout.begin() + nChangePosInOut,
tx_new->vout[nChangePosInOut]);
}
// Copy output sizes from new transaction; they may have had the fee
// subtracted from them.
for (size_t idx = 0; idx < tx.vout.size(); idx++) {
tx.vout[idx].nValue = tx_new->vout[idx].nValue;
}
// Add new txins (keeping original txin scriptSig/order)
for (const CTxIn &txin : tx_new->vin) {
if (!coinControl.IsSelected(txin.prevout)) {
tx.vin.push_back(txin);
}
if (lockUnspents) {
wallet.LockCoin(txin.prevout);
}
}
return true;
}
diff --git a/src/wallet/spend.h b/src/wallet/spend.h
index 0bdceb9e6..eaa02d54a 100644
--- a/src/wallet/spend.h
+++ b/src/wallet/spend.h
@@ -1,195 +1,196 @@
// Copyright (c) 2021 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_WALLET_SPEND_H
#define BITCOIN_WALLET_SPEND_H
+#include <util/result.h>
#include <wallet/coinselection.h>
#include <wallet/transaction.h>
#include <wallet/wallet.h>
#include <optional>
/**
* Get the marginal bytes if spending the specified output from this
* transaction
*/
int GetTxSpendSize(const CWallet &wallet, const CWalletTx &wtx,
unsigned int out, bool use_max_sig = false);
class COutput {
public:
const CWalletTx *tx;
int i;
int nDepth;
/**
* Pre-computed estimated size of this output as a fully-signed input in a
* transaction. Can be -1 if it could not be calculated.
*/
int nInputBytes;
/** Whether we have the private keys to spend this output */
bool fSpendable;
/** Whether we know how to spend this output, ignoring the lack of keys */
bool fSolvable;
/**
* Whether to use the maximum sized, 72 byte signature when calculating the
* size of the input spend. This should only be set when watch-only outputs
* are allowed.
*/
bool use_max_sig;
/**
* Whether this output is considered safe to spend. Unconfirmed transactions
* from outside keys are considered unsafe and will not be used to fund new
* spending transactions.
*/
bool fSafe;
COutput(const CWallet &wallet, const CWalletTx &wtx, int iIn, int nDepthIn,
bool fSpendableIn, bool fSolvableIn, bool fSafeIn,
bool use_max_sig_in = false) {
tx = &wtx;
i = iIn;
nDepth = nDepthIn;
fSpendable = fSpendableIn;
fSolvable = fSolvableIn;
fSafe = fSafeIn;
nInputBytes = -1;
use_max_sig = use_max_sig_in;
// If known and signable by the given wallet, compute nInputBytes
// Failure will keep this value -1
if (fSpendable) {
nInputBytes = GetTxSpendSize(wallet, wtx, i, use_max_sig);
}
}
std::string ToString() const;
inline CInputCoin GetInputCoin() const {
return CInputCoin(tx->tx, i, nInputBytes);
}
};
/** Get the marginal bytes of spending the specified output */
int CalculateMaximumSignedInputSize(const CTxOut &txout, const CWallet *pwallet,
bool use_max_sig = false);
/**
* Calculate the size of the transaction assuming all signatures are max size
* Use DummySignatureCreator, which inserts 71 byte signatures everywhere.
* NOTE: this requires that all inputs must be in mapWallet (eg the tx should
* be AllInputsMine).
*/
int64_t CalculateMaximumSignedTxSize(const CTransaction &tx,
const CWallet *wallet,
const std::vector<CTxOut> &txouts,
bool use_max_sig = false);
int64_t CalculateMaximumSignedTxSize(const CTransaction &tx,
const CWallet *wallet,
bool use_max_sig = false)
EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet);
/**
* populate vCoins with vector of available COutputs.
*/
void AvailableCoins(const CWallet &wallet, std::vector<COutput> &vCoins,
const CCoinControl *coinControl = nullptr,
const Amount nMinimumAmount = SATOSHI,
const Amount nMaximumAmount = MAX_MONEY,
const Amount nMinimumSumAmount = MAX_MONEY,
const uint64_t nMaximumCount = 0)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
Amount GetAvailableBalance(const CWallet &wallet,
const CCoinControl *coinControl = nullptr);
/**
* Find non-change parent output.
*/
const CTxOut &FindNonChangeParentOutput(const CWallet &wallet,
const CTransaction &tx, int output)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
/**
* Return list of available coins and locked coins grouped by non-change output
* address.
*/
std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet &wallet)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
std::vector<OutputGroup>
GroupOutputs(const CWallet &wallet, const std::vector<COutput> &outputs,
bool separate_coins, const CFeeRate &effective_feerate,
const CFeeRate &long_term_feerate,
const CoinEligibilityFilter &filter, bool positive_only);
/**
* Shuffle and select coins until nTargetValue is reached while avoiding
* small change; This method is stochastic for some inputs and upon
* completion the coin set and corresponding actual target value is
* assembled
* param@[in] groups Set of UTXOs to consider. These will be
* categorized into OutputGroups and filtered using eligibility_filter before
* selecting coins.
* param@[out] setCoinsRet Populated with the coins selected if successful.
* param@[out] nValueRet Used to return the total value of selected
* coins.
*/
bool SelectCoinsMinConf(const CWallet &wallet, const Amount nTargetValue,
const CoinEligibilityFilter &eligibility_filter,
std::vector<COutput> coins,
std::set<CInputCoin> &setCoinsRet, Amount &nValueRet,
const CoinSelectionParams &coin_selection_params,
bool &bnb_used);
/**
* Select a set of coins such that nValueRet >= nTargetValue and at least
* all coins from coin_control are selected; never select unconfirmed coins if
* they are not ours param@[out] setCoinsRet Populated with inputs
* including pre-selected inputs from coin_control and Coin Selection if
* successful. param@[out] nValueRet Total value of selected coins
* including pre-selected ones from coin_control and Coin Selection if
* successful.
*/
bool SelectCoins(const CWallet &wallet,
const std::vector<COutput> &vAvailableCoins,
const Amount nTargetValue, std::set<CInputCoin> &setCoinsRet,
Amount &nValueRet, const CCoinControl &coin_control,
CoinSelectionParams &coin_selection_params, bool &bnb_used)
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
struct CreatedTransactionResult {
CTransactionRef tx;
Amount fee;
int change_pos;
CreatedTransactionResult(CTransactionRef tx, Amount fee, int change_pos)
: tx(tx), fee(fee), change_pos(change_pos) {}
};
/**
* Create a new transaction paying the recipients with a set of coins
* selected by SelectCoins(); Also create the change output, when needed
* @note passing change_pos as -1 will result in setting a random position
*/
-std::optional<CreatedTransactionResult>
+util::Result<CreatedTransactionResult>
CreateTransaction(CWallet &wallet, const std::vector<CRecipient> &vecSend,
- int change_pos, bilingual_str &error,
- const CCoinControl &coin_control, bool sign = true);
+ int change_pos, const CCoinControl &coin_control,
+ bool sign = true);
/**
* Insert additional inputs into the transaction by
* calling CreateTransaction();
*/
bool FundTransaction(CWallet &wallet, CMutableTransaction &tx, Amount &nFeeRet,
int &nChangePosInOut, bilingual_str &error,
bool lockUnspents,
const std::set<int> &setSubtractFeeFromOutputs,
CCoinControl coinControl);
#endif // BITCOIN_WALLET_SPEND_H
diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
index b292fc063..bc90a26b9 100644
--- a/src/wallet/test/wallet_tests.cpp
+++ b/src/wallet/test/wallet_tests.cpp
@@ -1,945 +1,944 @@
// Copyright (c) 2012-2019 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <chain.h>
#include <chainparams.h>
#include <config.h>
#include <interfaces/chain.h>
#include <node/blockstorage.h>
#include <node/context.h>
#include <policy/policy.h>
#include <rpc/server.h>
#include <util/translation.h>
#include <validation.h>
#include <wallet/coincontrol.h>
#include <wallet/receive.h>
#include <wallet/rpc/backup.h>
#include <wallet/spend.h>
#include <wallet/wallet.h>
#include <test/util/logging.h>
#include <test/util/setup_common.h>
#include <wallet/test/wallet_test_fixture.h>
#include <boost/test/unit_test.hpp>
#include <univalue.h>
#include <any>
#include <cstdint>
#include <future>
#include <memory>
#include <variant>
#include <vector>
using node::MAX_BLOCKFILE_SIZE;
extern RecursiveMutex cs_wallets;
BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup)
static std::shared_ptr<CWallet> TestLoadWallet(interfaces::Chain *chain) {
DatabaseOptions options;
DatabaseStatus status;
bilingual_str error;
std::vector<bilingual_str> warnings;
auto database = MakeWalletDatabase("", options, status, error);
auto wallet = CWallet::Create(chain, "", std::move(database),
options.create_flags, error, warnings);
if (chain) {
wallet->postInitProcess();
}
return wallet;
}
static void TestUnloadWallet(std::shared_ptr<CWallet> &&wallet) {
SyncWithValidationInterfaceQueue();
wallet->m_chain_notifications_handler.reset();
UnloadWallet(std::move(wallet));
}
static CMutableTransaction TestSimpleSpend(const CTransaction &from,
uint32_t index, const CKey &key,
const CScript &pubkey) {
CMutableTransaction mtx;
mtx.vout.push_back(
{from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey});
mtx.vin.push_back({CTxIn{from.GetId(), index}});
FillableSigningProvider keystore;
keystore.AddKey(key);
std::map<COutPoint, Coin> coins;
coins[mtx.vin[0].prevout].GetTxOut() = from.vout[index];
std::map<int, std::string> input_errors;
BOOST_CHECK(SignTransaction(mtx, &keystore, coins,
SigHashType().withForkId(), input_errors));
return mtx;
}
static void AddKey(CWallet &wallet, const CKey &key) {
auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan();
LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore);
spk_man->AddKeyPubKey(key, key.GetPubKey());
}
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) {
ChainstateManager &chainman = *Assert(m_node.chainman);
// Cap last block file size, and mine new block in a new block file.
CBlockIndex *oldTip = WITH_LOCK(
chainman.GetMutex(), return m_node.chainman->ActiveChain().Tip());
WITH_LOCK(::cs_main, m_node.chainman->m_blockman
.GetBlockFileInfo(oldTip->GetBlockPos().nFile)
->nSize = MAX_BLOCKFILE_SIZE);
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
CBlockIndex *newTip = WITH_LOCK(
chainman.GetMutex(), return m_node.chainman->ActiveChain().Tip());
// Verify ScanForWalletTransactions fails to read an unknown start block.
{
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
{
LOCK(wallet.cs_wallet);
LOCK(chainman.GetMutex());
wallet.SetLastBlockProcessed(
m_node.chainman->ActiveHeight(),
m_node.chainman->ActiveTip()->GetBlockHash());
}
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
reserver.reserve();
CWallet::ScanResult result = wallet.ScanForWalletTransactions(
BlockHash() /* start_block */, 0 /* start_height */,
{} /* max_height */, reserver, false /* update */);
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
BOOST_CHECK(result.last_failed_block.IsNull());
BOOST_CHECK(result.last_scanned_block.IsNull());
BOOST_CHECK(!result.last_scanned_height);
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, Amount::zero());
}
// Verify ScanForWalletTransactions picks up transactions in both the old
// and new block files.
{
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
{
LOCK(wallet.cs_wallet);
LOCK(chainman.GetMutex());
wallet.SetLastBlockProcessed(
m_node.chainman->ActiveHeight(),
m_node.chainman->ActiveTip()->GetBlockHash());
}
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
reserver.reserve();
CWallet::ScanResult result = wallet.ScanForWalletTransactions(
oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */,
reserver, false /* update */);
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
BOOST_CHECK(result.last_failed_block.IsNull());
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 100 * COIN);
}
// Prune the older block file.
int file_number;
{
LOCK(cs_main);
file_number = oldTip->GetBlockPos().nFile;
Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
}
m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
// Verify ScanForWalletTransactions only picks transactions in the new block
// file.
{
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
{
LOCK(wallet.cs_wallet);
LOCK(chainman.GetMutex());
wallet.SetLastBlockProcessed(
m_node.chainman->ActiveHeight(),
m_node.chainman->ActiveTip()->GetBlockHash());
}
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
reserver.reserve();
CWallet::ScanResult result = wallet.ScanForWalletTransactions(
oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */,
reserver, false /* update */);
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash());
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 50 * COIN);
}
// Prune the remaining block file.
{
LOCK(cs_main);
file_number = newTip->GetBlockPos().nFile;
Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
}
m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
// Verify ScanForWalletTransactions scans no blocks.
{
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
{
LOCK(wallet.cs_wallet);
LOCK(chainman.GetMutex());
wallet.SetLastBlockProcessed(
m_node.chainman->ActiveHeight(),
m_node.chainman->ActiveTip()->GetBlockHash());
}
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
reserver.reserve();
CWallet::ScanResult result = wallet.ScanForWalletTransactions(
oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */,
reserver, false /* update */);
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash());
BOOST_CHECK(result.last_scanned_block.IsNull());
BOOST_CHECK(!result.last_scanned_height);
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, Amount::zero());
}
}
BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) {
ChainstateManager &chainman = *Assert(m_node.chainman);
// Cap last block file size, and mine new block in a new block file.
CBlockIndex *oldTip = WITH_LOCK(
chainman.GetMutex(), return m_node.chainman->ActiveChain().Tip());
WITH_LOCK(::cs_main, m_node.chainman->m_blockman
.GetBlockFileInfo(oldTip->GetBlockPos().nFile)
->nSize = MAX_BLOCKFILE_SIZE);
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
CBlockIndex *newTip = WITH_LOCK(
chainman.GetMutex(), return m_node.chainman->ActiveChain().Tip());
// Prune the older block file.
int file_number;
{
LOCK(cs_main);
file_number = oldTip->GetBlockPos().nFile;
chainman.m_blockman.PruneOneBlockFile(file_number);
}
m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
// Set this flag so that pwallet->chain().havePruned() returns true, which
// affects the RPC error message below.
m_node.chainman->m_blockman.m_have_pruned = true;
// Verify importmulti RPC returns failure for a key whose creation time is
// before the missing block, and success for a key whose creation time is
// after.
{
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(
m_node.chain.get(), "", CreateDummyWalletDatabase());
wallet->SetupLegacyScriptPubKeyMan();
WITH_LOCK(wallet->cs_wallet,
wallet->SetLastBlockProcessed(newTip->nHeight,
newTip->GetBlockHash()));
AddWallet(wallet);
UniValue keys;
keys.setArray();
UniValue key;
key.setObject();
key.pushKV("scriptPubKey",
HexStr(GetScriptForRawPubKey(coinbaseKey.GetPubKey())));
key.pushKV("timestamp", 0);
key.pushKV("internal", UniValue(true));
keys.push_back(key);
key.clear();
key.setObject();
CKey futureKey;
futureKey.MakeNewKey(true);
key.pushKV("scriptPubKey",
HexStr(GetScriptForRawPubKey(futureKey.GetPubKey())));
key.pushKV("timestamp",
newTip->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1);
key.pushKV("internal", UniValue(true));
keys.push_back(key);
JSONRPCRequest request;
request.params.setArray();
request.params.push_back(keys);
UniValue response = importmulti().HandleRequest(GetConfig(), request);
BOOST_CHECK_EQUAL(
response.write(),
strprintf("[{\"success\":false,\"error\":{\"code\":-1,\"message\":"
"\"Rescan failed for key with creation timestamp %d. "
"There was an error reading a block from time %d, which "
"is after or within %d seconds of key creation, and "
"could contain transactions pertaining to the key. As a "
"result, transactions and coins using this key may not "
"appear in the wallet. This error could be caused by "
"pruning or data corruption (see bitcoind log for "
"details) and could be dealt with by downloading and "
"rescanning the relevant blocks (see -reindex option "
"and rescanblockchain RPC).\"}},{\"success\":true}]",
0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW));
RemoveWallet(wallet, std::nullopt);
}
}
// Verify importwallet RPC starts rescan at earliest block with timestamp
// greater or equal than key birthday. Previously there was a bug where
// importwallet RPC would start the scan at the latest block with timestamp less
// than or equal to key birthday.
BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) {
ChainstateManager &chainman = *Assert(m_node.chainman);
// Create two blocks with same timestamp to verify that importwallet rescan
// will pick up both blocks, not just the first.
const int64_t BLOCK_TIME =
WITH_LOCK(chainman.GetMutex(),
return chainman.ActiveTip()->GetBlockTimeMax() + 5);
SetMockTime(BLOCK_TIME);
m_coinbase_txns.emplace_back(
CreateAndProcessBlock({},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
m_coinbase_txns.emplace_back(
CreateAndProcessBlock({},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
// Set key birthday to block time increased by the timestamp window, so
// rescan will start at the block time.
const int64_t KEY_TIME = BLOCK_TIME + TIMESTAMP_WINDOW;
SetMockTime(KEY_TIME);
m_coinbase_txns.emplace_back(
CreateAndProcessBlock({},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
std::string backup_file =
fs::PathToString(gArgs.GetDataDirNet() / "wallet.backup");
// Import key into wallet and call dumpwallet to create backup file.
{
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(
m_node.chain.get(), "", CreateDummyWalletDatabase());
{
auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan();
LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore);
spk_man->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()]
.nCreateTime = KEY_TIME;
spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey());
AddWallet(wallet);
LOCK(chainman.GetMutex());
wallet->SetLastBlockProcessed(chainman.ActiveHeight(),
chainman.ActiveTip()->GetBlockHash());
}
JSONRPCRequest request;
request.params.setArray();
request.params.push_back(backup_file);
::dumpwallet().HandleRequest(GetConfig(), request);
RemoveWallet(wallet, std::nullopt);
}
// Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME
// were scanned, and no prior blocks were scanned.
{
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(
m_node.chain.get(), "", CreateDummyWalletDatabase());
LOCK(wallet->cs_wallet);
wallet->SetupLegacyScriptPubKeyMan();
JSONRPCRequest request;
request.params.setArray();
request.params.push_back(backup_file);
AddWallet(wallet);
{
LOCK(chainman.GetMutex());
wallet->SetLastBlockProcessed(chainman.ActiveHeight(),
chainman.ActiveTip()->GetBlockHash());
}
::importwallet().HandleRequest(GetConfig(), request);
RemoveWallet(wallet, std::nullopt);
BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U);
BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U);
for (size_t i = 0; i < m_coinbase_txns.size(); ++i) {
bool found = wallet->GetWalletTx(m_coinbase_txns[i]->GetId());
bool expected = i >= 100;
BOOST_CHECK_EQUAL(found, expected);
}
}
}
// Check that GetImmatureCredit() returns a newly calculated value instead of
// the cached value after a MarkDirty() call.
//
// This is a regression test written to verify a bugfix for the immature credit
// function. Similar tests probably should be written for the other credit and
// debit functions.
BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup) {
ChainstateManager &chainman = *Assert(m_node.chainman);
CWallet wallet(m_node.chain.get(), "", CreateDummyWalletDatabase());
auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan();
CWalletTx wtx(m_coinbase_txns.back());
LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore);
LOCK(chainman.GetMutex());
wallet.SetLastBlockProcessed(chainman.ActiveHeight(),
chainman.ActiveTip()->GetBlockHash());
CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED,
chainman.ActiveHeight(),
chainman.ActiveTip()->GetBlockHash(), 0);
wtx.m_confirm = confirm;
// Call GetImmatureCredit() once before adding the key to the wallet to
// cache the current immature credit amount, which is 0.
BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx), Amount::zero());
// Invalidate the cached value, add the key, and make sure a new immature
// credit amount is calculated.
wtx.MarkDirty();
BOOST_CHECK(spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()));
BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx), 50 * COIN);
}
static int64_t AddTx(ChainstateManager &chainman, CWallet &wallet,
uint32_t lockTime, int64_t mockTime, int64_t blockTime) {
CMutableTransaction tx;
CWalletTx::Confirmation confirm;
tx.nLockTime = lockTime;
SetMockTime(mockTime);
CBlockIndex *block = nullptr;
if (blockTime > 0) {
LOCK(cs_main);
auto inserted = chainman.BlockIndex().emplace(
std::piecewise_construct, std::make_tuple(GetRandHash()),
std::make_tuple());
assert(inserted.second);
const BlockHash &hash = inserted.first->first;
block = &inserted.first->second;
block->nTime = blockTime;
block->phashBlock = &hash;
confirm = {CWalletTx::Status::CONFIRMED, block->nHeight, hash, 0};
}
// If transaction is already in map, to avoid inconsistencies,
// unconfirmation is needed before confirm again with different block.
return wallet
.AddToWallet(MakeTransactionRef(tx), confirm,
[&](CWalletTx &wtx, bool /* new_tx */) {
wtx.setUnconfirmed();
return true;
})
->nTimeSmart;
}
// Simple test to verify assignment of CWalletTx::nSmartTime value. Could be
// expanded to cover more corner cases of smart time logic.
BOOST_AUTO_TEST_CASE(ComputeTimeSmart) {
// New transaction should use clock time if lower than block time.
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 100, 120), 100);
// Test that updating existing transaction does not change smart time.
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200, 220), 100);
// New transaction should use clock time if there's no block time.
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300, 0), 300);
// New transaction should use block time if lower than clock time.
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 3, 420, 400), 400);
// New transaction should use latest entry time if higher than
// min(block time, clock time).
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 4, 500, 390), 400);
// If there are future entries, new transaction should use time of the
// newest entry that is no more than 300 seconds ahead of the clock time.
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300);
// Reset mock time for other tests.
SetMockTime(0);
}
BOOST_AUTO_TEST_CASE(LoadReceiveRequests) {
CTxDestination dest = PKHash();
LOCK(m_wallet.cs_wallet);
WalletBatch batch{m_wallet.GetDatabase()};
m_wallet.AddDestData(batch, dest, "misc", "val_misc");
m_wallet.AddDestData(batch, dest, "rr0", "val_rr0");
m_wallet.AddDestData(batch, dest, "rr1", "val_rr1");
auto values = m_wallet.GetDestValues("rr");
BOOST_CHECK_EQUAL(values.size(), 2U);
BOOST_CHECK_EQUAL(values[0], "val_rr0");
BOOST_CHECK_EQUAL(values[1], "val_rr1");
}
// Test some watch-only LegacyScriptPubKeyMan methods by the procedure of
// loading (LoadWatchOnly), checking (HaveWatchOnly), getting (GetWatchPubKey)
// and removing (RemoveWatchOnly) a given PubKey, resp. its corresponding P2PK
// Script. Results of the the impact on the address -> PubKey map is dependent
// on whether the PubKey is a point on the curve
static void TestWatchOnlyPubKey(LegacyScriptPubKeyMan *spk_man,
const CPubKey &add_pubkey) {
CScript p2pk = GetScriptForRawPubKey(add_pubkey);
CKeyID add_address = add_pubkey.GetID();
CPubKey found_pubkey;
LOCK(spk_man->cs_KeyStore);
// all Scripts (i.e. also all PubKeys) are added to the general watch-only
// set
BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
spk_man->LoadWatchOnly(p2pk);
BOOST_CHECK(spk_man->HaveWatchOnly(p2pk));
// only PubKeys on the curve shall be added to the watch-only address ->
// PubKey map
bool is_pubkey_fully_valid = add_pubkey.IsFullyValid();
if (is_pubkey_fully_valid) {
BOOST_CHECK(spk_man->GetWatchPubKey(add_address, found_pubkey));
BOOST_CHECK(found_pubkey == add_pubkey);
} else {
BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
// passed key is unchanged
BOOST_CHECK(found_pubkey == CPubKey());
}
spk_man->RemoveWatchOnly(p2pk);
BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
if (is_pubkey_fully_valid) {
BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
// passed key is unchanged
BOOST_CHECK(found_pubkey == add_pubkey);
}
}
// Cryptographically invalidate a PubKey whilst keeping length and first byte
static void PollutePubKey(CPubKey &pubkey) {
assert(pubkey.size() > 0);
std::vector<uint8_t> pubkey_raw(pubkey.begin(), pubkey.end());
std::fill(pubkey_raw.begin() + 1, pubkey_raw.end(), 0);
pubkey = CPubKey(pubkey_raw);
assert(!pubkey.IsFullyValid());
assert(pubkey.IsValid());
}
// Test watch-only logic for PubKeys
BOOST_AUTO_TEST_CASE(WatchOnlyPubKeys) {
CKey key;
CPubKey pubkey;
LegacyScriptPubKeyMan *spk_man =
m_wallet.GetOrCreateLegacyScriptPubKeyMan();
BOOST_CHECK(!spk_man->HaveWatchOnly());
// uncompressed valid PubKey
key.MakeNewKey(false);
pubkey = key.GetPubKey();
assert(!pubkey.IsCompressed());
TestWatchOnlyPubKey(spk_man, pubkey);
// uncompressed cryptographically invalid PubKey
PollutePubKey(pubkey);
TestWatchOnlyPubKey(spk_man, pubkey);
// compressed valid PubKey
key.MakeNewKey(true);
pubkey = key.GetPubKey();
assert(pubkey.IsCompressed());
TestWatchOnlyPubKey(spk_man, pubkey);
// compressed cryptographically invalid PubKey
PollutePubKey(pubkey);
TestWatchOnlyPubKey(spk_man, pubkey);
// invalid empty PubKey
pubkey = CPubKey();
TestWatchOnlyPubKey(spk_man, pubkey);
}
class ListCoinsTestingSetup : public TestChain100Setup {
public:
ListCoinsTestingSetup() {
ChainstateManager &chainman = *Assert(m_node.chainman);
CreateAndProcessBlock({},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
wallet = std::make_unique<CWallet>(m_node.chain.get(), "",
CreateMockWalletDatabase());
{
LOCK2(wallet->cs_wallet, ::cs_main);
wallet->SetLastBlockProcessed(chainman.ActiveHeight(),
chainman.ActiveTip()->GetBlockHash());
}
wallet->LoadWallet();
AddKey(*wallet, coinbaseKey);
WalletRescanReserver reserver(*wallet);
reserver.reserve();
CWallet::ScanResult result = wallet->ScanForWalletTransactions(
m_node.chainman->ActiveChain().Genesis()->GetBlockHash(),
0 /* start_height */, {} /* max_height */, reserver,
false /* update */);
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
LOCK(chainman.GetMutex());
BOOST_CHECK_EQUAL(result.last_scanned_block,
chainman.ActiveTip()->GetBlockHash());
BOOST_CHECK_EQUAL(*result.last_scanned_height, chainman.ActiveHeight());
BOOST_CHECK(result.last_failed_block.IsNull());
}
~ListCoinsTestingSetup() { wallet.reset(); }
CWalletTx &AddTx(CRecipient recipient) {
ChainstateManager &chainman = *Assert(m_node.chainman);
CTransactionRef tx;
- bilingual_str error;
CCoinControl dummy;
{
constexpr int RANDOM_CHANGE_POSITION = -1;
- std::optional<CreatedTransactionResult> txr = CreateTransaction(
- *wallet, {recipient}, RANDOM_CHANGE_POSITION, error, dummy);
- BOOST_CHECK(txr.has_value());
- tx = txr->tx;
+ auto res = CreateTransaction(*wallet, {recipient},
+ RANDOM_CHANGE_POSITION, dummy);
+ BOOST_CHECK(res);
+ tx = res->tx;
}
BOOST_CHECK_EQUAL(tx->nLockTime, 0);
wallet->CommitTransaction(tx, {}, {});
CMutableTransaction blocktx;
{
LOCK(wallet->cs_wallet);
blocktx =
CMutableTransaction(*wallet->mapWallet.at(tx->GetId()).tx);
}
CreateAndProcessBlock({CMutableTransaction(blocktx)},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
LOCK(wallet->cs_wallet);
LOCK(chainman.GetMutex());
wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1,
chainman.ActiveTip()->GetBlockHash());
auto it = wallet->mapWallet.find(tx->GetId());
BOOST_CHECK(it != wallet->mapWallet.end());
CWalletTx::Confirmation confirm(
CWalletTx::Status::CONFIRMED, chainman.ActiveHeight(),
chainman.ActiveTip()->GetBlockHash(), 1);
it->second.m_confirm = confirm;
return it->second;
}
std::unique_ptr<CWallet> wallet;
};
BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup) {
std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString();
// Confirm ListCoins initially returns 1 coin grouped under coinbaseKey
// address.
std::map<CTxDestination, std::vector<COutput>> list;
{
LOCK(wallet->cs_wallet);
list = ListCoins(*wallet);
}
BOOST_CHECK_EQUAL(list.size(), 1U);
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(),
coinbaseAddress);
BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U);
// Check initial balance from one mature coinbase transaction.
BOOST_CHECK_EQUAL(50 * COIN, GetAvailableBalance(*wallet));
// Add a transaction creating a change address, and confirm ListCoins still
// returns the coin associated with the change address underneath the
// coinbaseKey pubkey, even though the change address has a different
// pubkey.
AddTx(CRecipient{GetScriptForRawPubKey({}), 1 * COIN,
false /* subtract fee */});
{
LOCK(wallet->cs_wallet);
list = ListCoins(*wallet);
}
BOOST_CHECK_EQUAL(list.size(), 1U);
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(),
coinbaseAddress);
BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
// Lock both coins. Confirm number of available coins drops to 0.
{
LOCK(wallet->cs_wallet);
std::vector<COutput> available;
AvailableCoins(*wallet, available);
BOOST_CHECK_EQUAL(available.size(), 2U);
}
for (const auto &group : list) {
for (const auto &coin : group.second) {
LOCK(wallet->cs_wallet);
wallet->LockCoin(COutPoint(coin.tx->GetId(), coin.i));
}
}
{
LOCK(wallet->cs_wallet);
std::vector<COutput> available;
AvailableCoins(*wallet, available);
BOOST_CHECK_EQUAL(available.size(), 0U);
}
// Confirm ListCoins still returns same result as before, despite coins
// being locked.
{
LOCK(wallet->cs_wallet);
list = ListCoins(*wallet);
}
BOOST_CHECK_EQUAL(list.size(), 1U);
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(),
coinbaseAddress);
BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
}
BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) {
std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(
m_node.chain.get(), "", CreateDummyWalletDatabase());
wallet->SetupLegacyScriptPubKeyMan();
wallet->SetMinVersion(FEATURE_LATEST);
wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
BOOST_CHECK(!wallet->TopUpKeyPool(1000));
CTxDestination dest;
std::string error;
BOOST_CHECK(
!wallet->GetNewDestination(OutputType::LEGACY, "", dest, error));
}
// Explicit calculation which is used to test the wallet constant
static size_t CalculateP2PKHInputSize(bool use_max_sig) {
// Generate ephemeral valid pubkey
CKey key;
key.MakeNewKey(true);
CPubKey pubkey = key.GetPubKey();
// Generate pubkey hash
PKHash key_hash(pubkey);
// Create script to enter into keystore. Key hash can't be 0...
CScript script = GetScriptForDestination(key_hash);
// Add script to key store and key to watchonly
FillableSigningProvider keystore;
keystore.AddKeyPubKey(key, pubkey);
// Fill in dummy signatures for fee calculation.
SignatureData sig_data;
if (!ProduceSignature(keystore,
use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR
: DUMMY_SIGNATURE_CREATOR,
script, sig_data)) {
// We're hand-feeding it correct arguments; shouldn't happen
assert(false);
}
CTxIn tx_in;
UpdateInput(tx_in, sig_data);
return (size_t)GetVirtualTransactionInputSize(tx_in);
}
BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) {
BOOST_CHECK(CalculateP2PKHInputSize(false) <= DUMMY_P2PKH_INPUT_SIZE);
BOOST_CHECK_EQUAL(CalculateP2PKHInputSize(true), DUMMY_P2PKH_INPUT_SIZE);
}
bool malformed_descriptor(std::ios_base::failure e) {
std::string s(e.what());
return s.find("Missing checksum") != std::string::npos;
}
BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) {
std::vector<uint8_t> malformed_record;
CVectorWriter vw(0, 0, malformed_record, 0);
vw << std::string("notadescriptor");
vw << (uint64_t)0;
vw << (int32_t)0;
vw << (int32_t)0;
vw << (int32_t)1;
SpanReader vr{0, 0, malformed_record};
WalletDescriptor w_desc;
BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure,
malformed_descriptor);
}
//! Test CWallet::Create() and its behavior handling potential race
//! conditions if it's called the same time an incoming transaction shows up in
//! the mempool or a new block.
//!
//! It isn't possible to verify there aren't race condition in every case, so
//! this test just checks two specific cases and ensures that timing of
//! notifications in these cases doesn't prevent the wallet from detecting
//! transactions.
//!
//! In the first case, block and mempool transactions are created before the
//! wallet is loaded, but notifications about these transactions are delayed
//! until after it is loaded. The notifications are superfluous in this case, so
//! the test verifies the transactions are detected before they arrive.
//!
//! In the second case, block and mempool transactions are created after the
//! wallet rescan and notifications are immediately synced, to verify the wallet
//! must already have a handler in place for them, and there's no gap after
//! rescanning where new transactions in new blocks could be lost.
BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) {
// Create new wallet with known key and unload it.
auto wallet = TestLoadWallet(m_node.chain.get());
CKey key;
key.MakeNewKey(true);
AddKey(*wallet, key);
TestUnloadWallet(std::move(wallet));
// Add log hook to detect AddToWallet events from rescans, blockConnected,
// and transactionAddedToMempool notifications
int addtx_count = 0;
DebugLogHelper addtx_counter("[default wallet] AddToWallet",
[&](const std::string *s) {
if (s) {
++addtx_count;
}
return false;
});
bool rescan_completed = false;
DebugLogHelper rescan_check("[default wallet] Rescan completed",
[&](const std::string *s) {
if (s) {
rescan_completed = true;
}
return false;
});
// Block the queue to prevent the wallet receiving blockConnected and
// transactionAddedToMempool notifications, and create block and mempool
// transactions paying to the wallet
std::promise<void> promise;
CallFunctionInValidationInterfaceQueue(
[&promise] { promise.get_future().wait(); });
std::string error;
m_coinbase_txns.push_back(
CreateAndProcessBlock({},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey,
GetScriptForRawPubKey(key.GetPubKey()));
m_coinbase_txns.push_back(
CreateAndProcessBlock({block_tx},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey,
GetScriptForRawPubKey(key.GetPubKey()));
BOOST_CHECK(m_node.chain->broadcastTransaction(
GetConfig(), MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE,
false, error));
// Reload wallet and make sure new transactions are detected despite events
// being blocked
wallet = TestLoadWallet(m_node.chain.get());
BOOST_CHECK(rescan_completed);
BOOST_CHECK_EQUAL(addtx_count, 2);
{
LOCK(wallet->cs_wallet);
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetId()), 1U);
BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetId()), 1U);
}
// Unblock notification queue and make sure stale blockConnected and
// transactionAddedToMempool events are processed
promise.set_value();
SyncWithValidationInterfaceQueue();
BOOST_CHECK_EQUAL(addtx_count, 4);
TestUnloadWallet(std::move(wallet));
// Load wallet again, this time creating new block and mempool transactions
// paying to the wallet as the wallet finishes loading and syncing the
// queue so the events have to be handled immediately. Releasing the wallet
// lock during the sync is a little artificial but is needed to avoid a
// deadlock during the sync and simulates a new block notification happening
// as soon as possible.
addtx_count = 0;
auto handler = HandleLoadWallet(
[&](std::unique_ptr<interfaces::Wallet> wallet_param)
EXCLUSIVE_LOCKS_REQUIRED(wallet_param->wallet()->cs_wallet,
cs_wallets) {
BOOST_CHECK(rescan_completed);
m_coinbase_txns.push_back(
CreateAndProcessBlock(
{}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
block_tx =
TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey,
GetScriptForRawPubKey(key.GetPubKey()));
m_coinbase_txns.push_back(
CreateAndProcessBlock(
{block_tx},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
mempool_tx =
TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey,
GetScriptForRawPubKey(key.GetPubKey()));
BOOST_CHECK(m_node.chain->broadcastTransaction(
GetConfig(), MakeTransactionRef(mempool_tx),
DEFAULT_TRANSACTION_MAXFEE, false, error));
LEAVE_CRITICAL_SECTION(cs_wallets);
LEAVE_CRITICAL_SECTION(wallet_param->wallet()->cs_wallet);
SyncWithValidationInterfaceQueue();
ENTER_CRITICAL_SECTION(wallet_param->wallet()->cs_wallet);
ENTER_CRITICAL_SECTION(cs_wallets);
});
wallet = TestLoadWallet(m_node.chain.get());
BOOST_CHECK_EQUAL(addtx_count, 4);
{
LOCK(wallet->cs_wallet);
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetId()), 1U);
BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetId()), 1U);
}
TestUnloadWallet(std::move(wallet));
}
BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) {
auto wallet = TestLoadWallet(nullptr);
BOOST_CHECK(wallet);
UnloadWallet(std::move(wallet));
}
BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) {
auto wallet = TestLoadWallet(m_node.chain.get());
CKey key;
key.MakeNewKey(true);
AddKey(*wallet, key);
std::string error;
m_coinbase_txns.push_back(
CreateAndProcessBlock({},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()))
.vtx[0]);
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey,
GetScriptForRawPubKey(key.GetPubKey()));
CreateAndProcessBlock({block_tx},
GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
SyncWithValidationInterfaceQueue();
{
auto block_id = block_tx.GetId();
auto prev_id = m_coinbase_txns[0]->GetId();
LOCK(wallet->cs_wallet);
BOOST_CHECK(wallet->HasWalletSpend(prev_id));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_id), 1u);
std::vector<TxId> vIdIn{block_id}, vIdOut;
BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vIdIn, vIdOut),
DBErrors::LOAD_OK);
BOOST_CHECK(!wallet->HasWalletSpend(prev_id));
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_id), 0u);
}
TestUnloadWallet(std::move(wallet));
}
BOOST_AUTO_TEST_SUITE_END()

File Metadata

Mime Type
text/x-diff
Expires
Thu, May 22, 02:47 (14 h, 35 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5866328
Default Alt Text
(371 KB)

Event Timeline