Merge c27330897d into merged_master (Bitcoin PR #18027)

This commit is contained in:
Andrew Poelstra 2020-11-26 01:09:11 +00:00
commit 4839db857d
24 changed files with 592 additions and 93 deletions

View file

@ -35,6 +35,7 @@
<ClCompile Include="..\..\src\qt\paymentserver.cpp" />
<ClCompile Include="..\..\src\qt\peertablemodel.cpp" />
<ClCompile Include="..\..\src\qt\platformstyle.cpp" />
<ClCompile Include="..\..\src\qt\psbtoperationsdialog.cpp" />
<ClCompile Include="..\..\src\qt\qrimagewidget.cpp" />
<ClCompile Include="..\..\src\qt\qvalidatedlineedit.cpp" />
<ClCompile Include="..\..\src\qt\qvaluecombobox.cpp" />
@ -87,6 +88,7 @@
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_paymentserver.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_peertablemodel.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_platformstyle.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_psbtoperationsdialog.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_qrimagewidget.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_qvalidatedlineedit.cpp" />
<ClCompile Include="$(GeneratedFilesOutDir)\moc\moc_qvaluecombobox.cpp" />

View file

@ -25,6 +25,7 @@ QT_FORMS_UI = \
qt/forms/openuridialog.ui \
qt/forms/optionsdialog.ui \
qt/forms/overviewpage.ui \
qt/forms/psbtoperationsdialog.ui \
qt/forms/receivecoinsdialog.ui \
qt/forms/receiverequestdialog.ui \
qt/forms/debugwindow.ui \
@ -60,6 +61,7 @@ QT_MOC_CPP = \
qt/moc_optionsmodel.cpp \
qt/moc_overviewpage.cpp \
qt/moc_peertablemodel.cpp \
qt/moc_psbtoperationsdialog.cpp \
qt/moc_qrimagewidget.cpp \
qt/moc_qvalidatedlineedit.cpp \
qt/moc_qvaluecombobox.cpp \
@ -131,6 +133,7 @@ BITCOIN_QT_H = \
qt/overviewpage.h \
qt/peertablemodel.h \
qt/platformstyle.h \
qt/psbtoperationsdialog.h \
qt/qrimagewidget.h \
qt/qvalidatedlineedit.h \
qt/qvaluecombobox.h \
@ -240,6 +243,7 @@ BITCOIN_QT_WALLET_CPP = \
qt/editaddressdialog.cpp \
qt/openuridialog.cpp \
qt/overviewpage.cpp \
qt/psbtoperationsdialog.cpp \
qt/qrimagewidget.cpp \
qt/receivecoinsdialog.cpp \
qt/receiverequestdialog.cpp \

View file

@ -350,9 +350,10 @@ public:
bool sign,
bool bip32derivs,
PartiallySignedTransaction& psbtx,
bool& complete) override
bool& complete,
size_t* n_signed) override
{
return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs);
return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs, n_signed);
}
WalletBalances getBalances() override
{

View file

@ -199,7 +199,8 @@ public:
bool sign,
bool bip32derivs,
PartiallySignedTransaction& psbtx,
bool& complete) = 0;
bool& complete,
size_t* n_signed) = 0;
//! Get balances.
virtual WalletBalances getBalances() = 0;

View file

@ -235,6 +235,17 @@ bool PSBTInputSigned(const PSBTInput& input)
return !input.final_script_sig.empty() || !input.final_script_witness.IsNull();
}
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt) {
size_t count = 0;
for (const auto& input : psbt.inputs) {
if (!PSBTInputSigned(input)) {
count++;
}
}
return count;
}
void UpdatePSBTOutput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index)
{
const CTxOut& out = psbt.tx->vout.at(index);

View file

@ -966,6 +966,9 @@ bool PSBTInputSigned(const PSBTInput& input);
/** Signs a PSBTInput, verifying that all provided data matches what is being signed. */
bool SignPSBTInput(const SigningProvider& provider, PartiallySignedTransaction& psbt, int index, int sighash = SIGHASH_ALL, SignatureData* out_sigdata = nullptr, bool use_dummy = false);
/** Counts the unsigned inputs of a PSBT. */
size_t CountPSBTUnsignedInputs(const PartiallySignedTransaction& psbt);
/** Updates a PSBTOutput with information from provider.
*
* This fills in the redeem_script, witness_script, and hd_keypaths where possible.

View file

@ -321,8 +321,10 @@ void BitcoinGUI::createActions()
signMessageAction->setStatusTip(tr("Sign messages with your %1 addresses to prove you own them").arg("Liquid"));
verifyMessageAction = new QAction(tr("&Verify message..."), this);
verifyMessageAction->setStatusTip(tr("Verify messages to ensure they were signed with specified %1 addresses").arg("Liquid"));
m_load_psbt_action = new QAction(tr("Load PSET..."), this);
m_load_psbt_action = new QAction(tr("&Load PSBT from file..."), this);
m_load_psbt_action->setStatusTip(tr("Load Partially Signed Elements Transaction"));
m_load_psbt_clipboard_action = new QAction(tr("Load PSBT from clipboard..."), this);
m_load_psbt_clipboard_action->setStatusTip(tr("Load Partially Signed Elements Transaction from clipboard"));
openRPCConsoleAction = new QAction(tr("Node window"), this);
openRPCConsoleAction->setStatusTip(tr("Open node debugging and diagnostic console"));
@ -378,6 +380,7 @@ void BitcoinGUI::createActions()
connect(signMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
connect(signMessageAction, &QAction::triggered, [this]{ gotoSignMessageTab(); });
connect(m_load_psbt_action, &QAction::triggered, [this]{ gotoLoadPSBT(); });
connect(m_load_psbt_clipboard_action, &QAction::triggered, [this]{ gotoLoadPSBT(true); });
connect(verifyMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
connect(verifyMessageAction, &QAction::triggered, [this]{ gotoVerifyMessageTab(); });
connect(usedSendingAddressesAction, &QAction::triggered, walletFrame, &WalletFrame::usedSendingAddresses);
@ -454,6 +457,7 @@ void BitcoinGUI::createMenuBar()
file->addAction(signMessageAction);
file->addAction(verifyMessageAction);
file->addAction(m_load_psbt_action);
file->addAction(m_load_psbt_clipboard_action);
file->addSeparator();
}
file->addAction(quitAction);
@ -863,9 +867,9 @@ void BitcoinGUI::gotoVerifyMessageTab(QString addr)
{
if (walletFrame) walletFrame->gotoVerifyMessageTab(addr);
}
void BitcoinGUI::gotoLoadPSBT()
void BitcoinGUI::gotoLoadPSBT(bool from_clipboard)
{
if (walletFrame) walletFrame->gotoLoadPSBT();
if (walletFrame) walletFrame->gotoLoadPSBT(from_clipboard);
}
#endif // ENABLE_WALLET

View file

@ -138,6 +138,7 @@ private:
QAction* signMessageAction = nullptr;
QAction* verifyMessageAction = nullptr;
QAction* m_load_psbt_action = nullptr;
QAction* m_load_psbt_clipboard_action = nullptr;
QAction* aboutAction = nullptr;
QAction* receiveCoinsAction = nullptr;
QAction* receiveCoinsMenuAction = nullptr;
@ -275,8 +276,8 @@ public Q_SLOTS:
void gotoSignMessageTab(QString addr = "");
/** Show Sign/Verify Message dialog and switch to verify message tab */
void gotoVerifyMessageTab(QString addr = "");
/** Show load Partially Signed Bitcoin Transaction dialog */
void gotoLoadPSBT();
/** Load Partially Signed Bitcoin Transaction from file or clipboard */
void gotoLoadPSBT(bool from_clipboard = false);
#endif // ENABLE_WALLET
/** Show configuration dialog */

View file

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PSBTOperationsDialog</class>
<widget class="QDialog" name="PSBTOperationsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>585</width>
<height>327</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>12</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QVBoxLayout" name="mainDialogLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="statusBar">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="transactionDescription">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="buttonRowLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="signTransactionButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Sign Tx</string>
</property>
<property name="autoDefault">
<bool>true</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="broadcastTransactionButton">
<property name="text">
<string>Broadcast Tx</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="copyToClipboardButton">
<property name="text">
<string>Copy to Clipboard</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="saveButton">
<property name="text">
<string>Save...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -0,0 +1,270 @@
// Copyright (c) 2011-2020 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/psbtoperationsdialog.h>
#include <core_io.h>
#include <interfaces/node.h>
#include <key_io.h>
#include <node/psbt.h>
#include <policy/policy.h>
#include <qt/bitcoinunits.h>
#include <qt/forms/ui_psbtoperationsdialog.h>
#include <qt/guiutil.h>
#include <qt/optionsmodel.h>
#include <util/strencodings.h>
#include <iostream>
PSBTOperationsDialog::PSBTOperationsDialog(
QWidget* parent, WalletModel* wallet_model, ClientModel* client_model) : QDialog(parent),
m_ui(new Ui::PSBTOperationsDialog),
m_wallet_model(wallet_model),
m_client_model(client_model)
{
m_ui->setupUi(this);
setWindowTitle("PSBT Operations");
connect(m_ui->signTransactionButton, &QPushButton::clicked, this, &PSBTOperationsDialog::signTransaction);
connect(m_ui->broadcastTransactionButton, &QPushButton::clicked, this, &PSBTOperationsDialog::broadcastTransaction);
connect(m_ui->copyToClipboardButton, &QPushButton::clicked, this, &PSBTOperationsDialog::copyToClipboard);
connect(m_ui->saveButton, &QPushButton::clicked, this, &PSBTOperationsDialog::saveTransaction);
connect(m_ui->closeButton, &QPushButton::clicked, this, &PSBTOperationsDialog::close);
m_ui->signTransactionButton->setEnabled(false);
m_ui->broadcastTransactionButton->setEnabled(false);
}
PSBTOperationsDialog::~PSBTOperationsDialog()
{
delete m_ui;
}
void PSBTOperationsDialog::openWithPSBT(PartiallySignedTransaction psbtx)
{
m_transaction_data = psbtx;
bool complete;
size_t n_could_sign;
FinalizePSBT(psbtx); // Make sure all existing signatures are fully combined before checking for completeness.
TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, m_transaction_data, complete, &n_could_sign);
if (err != TransactionError::OK) {
showStatus(tr("Failed to load transaction: %1")
.arg(QString::fromStdString(TransactionErrorString(err).translated)), StatusLevel::ERR);
return;
}
m_ui->broadcastTransactionButton->setEnabled(complete);
m_ui->signTransactionButton->setEnabled(!complete && !m_wallet_model->wallet().privateKeysDisabled() && n_could_sign > 0);
updateTransactionDisplay();
}
void PSBTOperationsDialog::signTransaction()
{
bool complete;
size_t n_signed;
TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, m_transaction_data, complete, &n_signed);
if (err != TransactionError::OK) {
showStatus(tr("Failed to sign transaction: %1")
.arg(QString::fromStdString(TransactionErrorString(err).translated)), StatusLevel::ERR);
return;
}
updateTransactionDisplay();
if (!complete && n_signed < 1) {
showStatus(tr("Could not sign any more inputs."), StatusLevel::WARN);
} else if (!complete) {
showStatus(tr("Signed %1 inputs, but more signatures are still required.").arg(n_signed),
StatusLevel::INFO);
} else {
showStatus(tr("Signed transaction successfully. Transaction is ready to broadcast."),
StatusLevel::INFO);
m_ui->broadcastTransactionButton->setEnabled(true);
}
}
void PSBTOperationsDialog::broadcastTransaction()
{
CMutableTransaction mtx;
if (!FinalizeAndExtractPSBT(m_transaction_data, mtx)) {
// This is never expected to fail unless we were given a malformed PSBT
// (e.g. with an invalid signature.)
showStatus(tr("Unknown error processing transaction."), StatusLevel::ERR);
return;
}
CTransactionRef tx = MakeTransactionRef(mtx);
std::string err_string;
TransactionError error = BroadcastTransaction(
*m_client_model->node().context(), tx, err_string, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), /* relay */ true, /* await_callback */ false);
if (error == TransactionError::OK) {
showStatus(tr("Transaction broadcast successfully! Transaction ID: %1")
.arg(QString::fromStdString(tx->GetHash().GetHex())), StatusLevel::INFO);
} else {
showStatus(tr("Transaction broadcast failed: %1")
.arg(QString::fromStdString(TransactionErrorString(error).translated)), StatusLevel::ERR);
}
}
void PSBTOperationsDialog::copyToClipboard() {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << m_transaction_data;
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
showStatus(tr("PSBT copied to clipboard."), StatusLevel::INFO);
}
void PSBTOperationsDialog::saveTransaction() {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << m_transaction_data;
QString selected_filter;
QString filename_suggestion = "";
bool first = true;
for (const CTxOut& out : m_transaction_data.tx->vout) {
if (!first) {
filename_suggestion.append("-");
}
CTxDestination address;
ExtractDestination(out.scriptPubKey, address);
QString amount = BitcoinUnits::format(m_wallet_model->getOptionsModel()->getDisplayUnit(), out.nValue.GetAmount());
QString address_str = QString::fromStdString(EncodeDestination(address));
filename_suggestion.append(address_str + "-" + amount);
first = false;
}
filename_suggestion.append(".psbt");
QString filename = GUIUtil::getSaveFileName(this,
tr("Save Transaction Data"), filename_suggestion,
tr("Partially Signed Transaction (Binary) (*.psbt)"), &selected_filter);
if (filename.isEmpty()) {
return;
}
std::ofstream out(filename.toLocal8Bit().data());
out << ssTx.str();
out.close();
showStatus(tr("PSBT saved to disk."), StatusLevel::INFO);
}
void PSBTOperationsDialog::updateTransactionDisplay() {
m_ui->transactionDescription->setText(QString::fromStdString(renderTransaction(m_transaction_data)));
showTransactionStatus(m_transaction_data);
}
std::string PSBTOperationsDialog::renderTransaction(const PartiallySignedTransaction &psbtx)
{
QString tx_description = "";
CAmount totalAmount = 0;
int unit = m_client_model->getOptionsModel()->getDisplayUnit();
for (const CTxOut& out : psbtx.tx->vout) {
CTxDestination address;
ExtractDestination(out.scriptPubKey, address);
totalAmount += out.nValue.GetAmount();
tx_description.append(tr(" * Sends %1 to %2")
.arg(GUIUtil::formatAssetAmount(out.nAsset.GetAsset(), out.nValue.GetAmount(), unit, BitcoinUnits::SeparatorStyle::ALWAYS))
.arg(QString::fromStdString(EncodeDestination(address))));
tx_description.append("<br>");
}
PSBTAnalysis analysis = AnalyzePSBT(psbtx);
tx_description.append(" * ");
if (!*analysis.fee) {
// This happens if the transaction is missing input UTXO information.
tx_description.append(tr("Unable to calculate transaction fee or total transaction amount."));
} else {
// FIXME: should we display non-bitcoin fees?
tx_description.append(tr("Pays transaction fee: "));
tx_description.append(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, (*analysis.fee)[::policyAsset]));
// add total amount in all subdivision units
tx_description.append("<hr />");
QStringList alternativeUnits;
for (const BitcoinUnits::Unit u : BitcoinUnits::availableUnits())
{
if(u != m_client_model->getOptionsModel()->getDisplayUnit()) {
alternativeUnits.append(BitcoinUnits::formatHtmlWithUnit(u, totalAmount));
}
}
tx_description.append(QString("<b>%1</b>: <b>%2</b>").arg(tr("Total Amount"))
.arg(BitcoinUnits::formatHtmlWithUnit(m_client_model->getOptionsModel()->getDisplayUnit(), totalAmount)));
tx_description.append(QString("<br /><span style='font-size:10pt; font-weight:normal;'>(=%1)</span>")
.arg(alternativeUnits.join(" " + tr("or") + " ")));
}
size_t num_unsigned = CountPSBTUnsignedInputs(psbtx);
if (num_unsigned > 0) {
tx_description.append("<br><br>");
tx_description.append(tr("Transaction has %1 unsigned inputs.").arg(QString::number(num_unsigned)));
}
return tx_description.toStdString();
}
void PSBTOperationsDialog::showStatus(const QString &msg, StatusLevel level) {
m_ui->statusBar->setText(msg);
switch (level) {
case StatusLevel::INFO: {
m_ui->statusBar->setStyleSheet("QLabel { background-color : lightgreen }");
break;
}
case StatusLevel::WARN: {
m_ui->statusBar->setStyleSheet("QLabel { background-color : orange }");
break;
}
case StatusLevel::ERR: {
m_ui->statusBar->setStyleSheet("QLabel { background-color : red }");
break;
}
}
m_ui->statusBar->show();
}
size_t PSBTOperationsDialog::couldSignInputs(const PartiallySignedTransaction &psbtx) {
size_t n_signed;
bool complete;
TransactionError err = m_wallet_model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, false /* bip32derivs */, m_transaction_data, complete, &n_signed);
if (err != TransactionError::OK) {
return 0;
}
return n_signed;
}
void PSBTOperationsDialog::showTransactionStatus(const PartiallySignedTransaction &psbtx) {
PSBTAnalysis analysis = AnalyzePSBT(psbtx);
size_t n_could_sign = couldSignInputs(psbtx);
switch (analysis.next) {
case PSBTRole::UPDATER: {
showStatus(tr("Transaction is missing some information about inputs."), StatusLevel::WARN);
break;
}
case PSBTRole::SIGNER: {
QString need_sig_text = tr("Transaction still needs signature(s).");
StatusLevel level = StatusLevel::INFO;
if (m_wallet_model->wallet().privateKeysDisabled()) {
need_sig_text += " " + tr("(But this wallet cannot sign transactions.)");
level = StatusLevel::WARN;
} else if (n_could_sign < 1) {
need_sig_text += " " + tr("(But this wallet does not have the right keys.)"); // XXX wording
level = StatusLevel::WARN;
}
showStatus(need_sig_text, level);
break;
}
case PSBTRole::FINALIZER:
case PSBTRole::EXTRACTOR: {
showStatus(tr("Transaction is fully signed and ready for broadcast."), StatusLevel::INFO);
break;
}
default: {
showStatus(tr("Transaction status is unknown."), StatusLevel::ERR);
break;
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2011-2020 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_QT_PSBTOPERATIONSDIALOG_H
#define BITCOIN_QT_PSBTOPERATIONSDIALOG_H
#include <QDialog>
#include <psbt.h>
#include <qt/clientmodel.h>
#include <qt/walletmodel.h>
namespace Ui {
class PSBTOperationsDialog;
}
/** Dialog showing transaction details. */
class PSBTOperationsDialog : public QDialog
{
Q_OBJECT
public:
explicit PSBTOperationsDialog(QWidget* parent, WalletModel* walletModel, ClientModel* clientModel);
~PSBTOperationsDialog();
void openWithPSBT(PartiallySignedTransaction psbtx);
public Q_SLOTS:
void signTransaction();
void broadcastTransaction();
void copyToClipboard();
void saveTransaction();
private:
Ui::PSBTOperationsDialog* m_ui;
PartiallySignedTransaction m_transaction_data;
WalletModel* m_wallet_model;
ClientModel* m_client_model;
enum class StatusLevel {
INFO,
WARN,
ERR
};
size_t couldSignInputs(const PartiallySignedTransaction &psbtx);
void updateTransactionDisplay();
std::string renderTransaction(const PartiallySignedTransaction &psbtx);
void showStatus(const QString &msg, StatusLevel level);
void showTransactionStatus(const PartiallySignedTransaction &psbtx);
};
#endif // BITCOIN_QT_PSBTOPERATIONSDIALOG_H

View file

@ -399,7 +399,7 @@ void SendCoinsDialog::on_sendButton_clicked()
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete);
const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete, nullptr);
assert(!complete);
assert(err == TransactionError::OK);
// Serialize the PSBT

View file

@ -165,11 +165,11 @@ void WalletFrame::gotoVerifyMessageTab(QString addr)
walletView->gotoVerifyMessageTab(addr);
}
void WalletFrame::gotoLoadPSBT()
void WalletFrame::gotoLoadPSBT(bool from_clipboard)
{
WalletView *walletView = currentWalletView();
if (walletView) {
walletView->gotoLoadPSBT();
walletView->gotoLoadPSBT(from_clipboard);
}
}

View file

@ -79,7 +79,7 @@ public Q_SLOTS:
void gotoVerifyMessageTab(QString addr = "");
/** Load Partially Signed Bitcoin Transaction */
void gotoLoadPSBT();
void gotoLoadPSBT(bool from_clipboard = false);
/** Encrypt the wallet */
void encryptWallet(bool status);

View file

@ -571,7 +571,7 @@ bool WalletModel::bumpFee(uint256 hash, uint256& new_hash)
if (create_psbt) {
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
const TransactionError err = wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete);
const TransactionError err = wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete, nullptr);
if (err != TransactionError::OK || complete) {
QMessageBox::critical(nullptr, tr("Fee bump error"), tr("Can't draft transaction."));
return false;

View file

@ -11,6 +11,7 @@
#include <qt/askpassphrasedialog.h>
#include <qt/clientmodel.h>
#include <qt/guiutil.h>
#include <qt/psbtoperationsdialog.h>
#include <qt/optionsmodel.h>
#include <qt/overviewpage.h>
#include <qt/platformstyle.h>
@ -27,6 +28,8 @@
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QClipboard>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QProgressDialog>
@ -203,78 +206,42 @@ void WalletView::gotoVerifyMessageTab(QString addr)
signVerifyMessageDialog->setAddress_VM(addr);
}
void WalletView::gotoLoadPSBT()
void WalletView::gotoLoadPSBT(bool from_clipboard)
{
QString filename = GUIUtil::getOpenFileName(this,
tr("Load Transaction Data"), QString(),
tr("Partially Signed Transaction (*.psbt)"), nullptr);
if (filename.isEmpty()) return;
if (GetFileSize(filename.toLocal8Bit().data(), MAX_FILE_SIZE_PSBT) == MAX_FILE_SIZE_PSBT) {
Q_EMIT message(tr("Error"), tr("PSBT file must be smaller than 100 MiB"), CClientUIInterface::MSG_ERROR);
return;
std::string data;
if (from_clipboard) {
std::string raw = QApplication::clipboard()->text().toStdString();
bool invalid;
data = DecodeBase64(raw, &invalid);
if (invalid) {
Q_EMIT message(tr("Error"), tr("Unable to decode PSBT from clipboard (invalid base64)"), CClientUIInterface::MSG_ERROR);
return;
}
} else {
QString filename = GUIUtil::getOpenFileName(this,
tr("Load Transaction Data"), QString(),
tr("Partially Signed Transaction (*.psbt)"), nullptr);
if (filename.isEmpty()) return;
if (GetFileSize(filename.toLocal8Bit().data(), MAX_FILE_SIZE_PSBT) == MAX_FILE_SIZE_PSBT) {
Q_EMIT message(tr("Error"), tr("PSBT file must be smaller than 100 MiB"), CClientUIInterface::MSG_ERROR);
return;
}
std::ifstream in(filename.toLocal8Bit().data(), std::ios::binary);
data = std::string(std::istreambuf_iterator<char>{in}, {});
}
std::ifstream in(filename.toLocal8Bit().data(), std::ios::binary);
std::string data(std::istreambuf_iterator<char>{in}, {});
std::string error;
PartiallySignedTransaction psbtx;
if (!DecodeRawPSBT(psbtx, data, error)) {
Q_EMIT message(tr("Error"), tr("Unable to decode PSBT file") + "\n" + QString::fromStdString(error), CClientUIInterface::MSG_ERROR);
Q_EMIT message(tr("Error"), tr("Unable to decode PSBT") + "\n" + QString::fromStdString(error), CClientUIInterface::MSG_ERROR);
return;
}
CMutableTransaction mtx;
bool complete = false;
PSBTAnalysis analysis = AnalyzePSBT(psbtx);
QMessageBox msgBox;
msgBox.setText("PSBT");
switch (analysis.next) {
case PSBTRole::CREATOR:
case PSBTRole::UPDATER:
msgBox.setInformativeText("PSBT is incomplete. Copy to clipboard for manual inspection?");
break;
case PSBTRole::SIGNER:
msgBox.setInformativeText("Transaction needs more signatures. Copy to clipboard?");
break;
case PSBTRole::FINALIZER:
case PSBTRole::EXTRACTOR:
complete = FinalizeAndExtractPSBT(psbtx, mtx);
if (complete) {
msgBox.setInformativeText(tr("Would you like to send this transaction?"));
} else {
// The analyzer missed something, e.g. if there are final_scriptSig/final_scriptWitness
// but with invalid signatures.
msgBox.setInformativeText(tr("There was an unexpected problem processing the PSBT. Copy to clipboard for manual inspection?"));
}
}
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
switch (msgBox.exec()) {
case QMessageBox::Yes: {
if (complete) {
std::string err_string;
CTransactionRef tx = MakeTransactionRef(mtx);
TransactionError result = BroadcastTransaction(*clientModel->node().context(), tx, err_string, DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(), /* relay */ true, /* wait_callback */ false);
if (result == TransactionError::OK) {
Q_EMIT message(tr("Success"), tr("Broadcasted transaction successfully."), CClientUIInterface::MSG_INFORMATION | CClientUIInterface::MODAL);
} else {
Q_EMIT message(tr("Error"), QString::fromStdString(err_string), CClientUIInterface::MSG_ERROR);
}
} else {
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
Q_EMIT message(tr("PSBT copied"), "Copied to clipboard", CClientUIInterface::MSG_INFORMATION);
return;
}
}
case QMessageBox::Cancel:
break;
default:
assert(false);
}
PSBTOperationsDialog* dlg = new PSBTOperationsDialog(this, walletModel, clientModel);
dlg->openWithPSBT(psbtx);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->exec();
}
bool WalletView::handlePaymentRequest(const SendCoinsRecipient& recipient)

View file

@ -84,7 +84,7 @@ public Q_SLOTS:
/** Show Sign/Verify Message dialog and switch to verify message tab */
void gotoVerifyMessageTab(QString addr = "");
/** Load Partially Signed Bitcoin Transaction */
void gotoLoadPSBT();
void gotoLoadPSBT(bool from_clipboard = false);
/** Show incoming transaction notification for new transactions.

View file

@ -14,7 +14,7 @@ bilingual_str TransactionErrorString(const TransactionError err)
case TransactionError::OK:
return Untranslated("No error");
case TransactionError::MISSING_INPUTS:
return Untranslated("Missing inputs");
return Untranslated("Inputs missing or spent");
case TransactionError::ALREADY_IN_CHAIN:
return Untranslated("Transaction already in block chain");
case TransactionError::P2P_DISABLED:
@ -24,11 +24,11 @@ bilingual_str TransactionErrorString(const TransactionError err)
case TransactionError::MEMPOOL_ERROR:
return Untranslated("AcceptToMemoryPool failed");
case TransactionError::INVALID_PSBT:
return Untranslated("PSBT is not sane");
return Untranslated("PSBT is not well-formed");
case TransactionError::PSBT_MISMATCH:
return Untranslated("PSBTs not compatible (different transactions)");
case TransactionError::SIGHASH_MISMATCH:
return Untranslated("Specified sighash value does not match existing value");
return Untranslated("Specified sighash value does not match value stored in PSBT");
case TransactionError::MAX_FEE_EXCEEDED:
return Untranslated("Fee exceeds maximum configured by -maxtxfee");
case TransactionError::BLINDING_REQUIRED:

View file

@ -591,8 +591,11 @@ SigningResult LegacyScriptPubKeyMan::SignMessage(const std::string& message, con
return SigningResult::SIGNING_FAILED;
}
TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs) const
TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (n_signed) {
*n_signed = 0;
}
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
const CTxIn& txin = psbtx.tx->vin[i];
PSBTInput& input = psbtx.inputs.at(i);
@ -623,6 +626,14 @@ TransactionError LegacyScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psb
SignatureData sigdata;
input.FillSignatureData(sigdata);
SignPSBTInput(HidingSigningProvider(this, !sign, !bip32derivs), psbtx, i, sighash_type);
bool signed_one = PSBTInputSigned(input);
if (n_signed && (signed_one || !sign)) {
// If sign is false, we assume that we _could_ sign if we get here. This
// will never have false negatives; it is hard to tell under what i
// circumstances it could have false positives.
(*n_signed)++;
}
}
// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change
@ -2069,8 +2080,11 @@ SigningResult DescriptorScriptPubKeyMan::SignMessage(const std::string& message,
return SigningResult::OK;
}
TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs) const
TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, int sighash_type, bool sign, bool bip32derivs, int* n_signed) const
{
if (n_signed) {
*n_signed = 0;
}
for (unsigned int i = 0; i < psbtx.tx->vin.size(); ++i) {
const CTxIn& txin = psbtx.tx->vin[i];
PSBTInput& input = psbtx.inputs.at(i);
@ -2122,6 +2136,14 @@ TransactionError DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction&
}
SignPSBTInput(HidingSigningProvider(keys.get(), !sign, !bip32derivs), psbtx, i, sighash_type);
bool signed_one = PSBTInputSigned(input);
if (n_signed && (signed_one || !sign)) {
// If sign is false, we assume that we _could_ sign if we get here. This
// will never have false negatives; it is hard to tell under what i
// circumstances it could have false positives.
(*n_signed)++;
}
}
// Fill in the bip32 keypaths and redeemscripts for the outputs so that hardware wallets can identify change

View file

@ -234,7 +234,7 @@ public:
/** Sign a message with the given script */
virtual SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const { return SigningResult::SIGNING_FAILED; };
/** Adds script and derivation path information to a PSBT, and optionally signs it. */
virtual TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false) const { return TransactionError::INVALID_PSBT; }
virtual TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const { return TransactionError::INVALID_PSBT; }
virtual uint256 GetID() const { return uint256(); }
@ -393,7 +393,7 @@ public:
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
uint256 GetID() const override;
@ -602,7 +602,7 @@ public:
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, std::string>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false) const override;
TransactionError FillPSBT(PartiallySignedTransaction& psbt, int sighash_type = 1 /* SIGHASH_ALL */, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr) const override;
uint256 GetID() const override;

View file

@ -2757,7 +2757,7 @@ TransactionError CWallet::FillPSBTData(PartiallySignedTransaction& psbtx, bool b
return TransactionError::OK;
}
TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool imbalance_ok, bool bip32derivs) const
TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool imbalance_ok, bool bip32derivs, size_t* n_signed) const
{
// If we're signing, check that the transaction is not still in need of blinding
if (sign) {
@ -2768,6 +2768,10 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp
}
}
if (n_signed) {
*n_signed = 0;
}
// Save the original transaction since we need to munge it temporarily, which would violate the PSBT rules
CTransaction oldtx = CTransaction(*psbtx.tx);
@ -2850,11 +2854,16 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp
}
for (ScriptPubKeyMan* spk_man : GetAllScriptPubKeyMans()) {
int n_signed_this_spkm = 0;
// ELEMENTS: Here we _only_ sign, and do not e.g. fill in key origin data.
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs);
TransactionError res = spk_man->FillPSBT(psbtx, sighash_type, sign, bip32derivs, &n_signed_this_spkm);
if (res != TransactionError::OK) {
return res;
}
if (n_signed) {
(*n_signed) += n_signed_this_spkm;
}
}
// Complete if every input is now signed
@ -2869,7 +2878,7 @@ TransactionError CWallet::SignPSBT(PartiallySignedTransaction& psbtx, bool& comp
}
// This function remains for backwards compatibility. It will not succeed in Elements unless everything involved is non-blinded.
TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool bip32derivs) const
TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type, bool sign, bool bip32derivs, size_t* n_signed) const
{
complete = false;
TransactionError te;
@ -2878,7 +2887,7 @@ TransactionError CWallet::FillPSBT(PartiallySignedTransaction& psbtx, bool& comp
return te;
}
// For backwards compatibility, do not check if amounts balance before signing in this case.
te = SignPSBT(psbtx, complete, sighash_type, sign, true, bip32derivs);
te = SignPSBT(psbtx, complete, sighash_type, sign, true, bip32derivs, n_signed);
if (te != TransactionError::OK) {
return te;
}

View file

@ -1077,11 +1077,12 @@ public:
bool& complete,
int sighash_type = 1 /* SIGHASH_ALL */,
bool sign = true,
bool bip32derivs = true) const;
bool bip32derivs = true,
size_t* n_signed = nullptr) const;
// ELEMENTS
TransactionError FillPSBTData(PartiallySignedTransaction& psbtx, bool bip32derivs = false) const;
TransactionError SignPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type = 1, bool sign = true, bool imbalance_ok = false, bool bip32derivs = false) const;
TransactionError SignPSBT(PartiallySignedTransaction& psbtx, bool& complete, int sighash_type = 1, bool sign = true, bool imbalance_ok = false, bool bip32derivs = false, size_t* n_signed = nullptr) const;
// end ELEMENTS
/**

View file

@ -588,7 +588,7 @@ class PSBTTest(BitcoinTestFramework):
self.nodes[0].generate(200)
self.sync_all()
assert_raises_rpc_error(-25, 'Missing inputs', self.nodes[0].walletprocesspsbt, 'cHNldP8BAOECAAAAAAK2GUhaoOw0nV0BJhZg0a8G7vb9PQvlTds8GA/N+l+peAMAAAAA/////7YZSFqg7DSdXQEmFmDRrwbu9v09C+VN2zwYD836X6l4AQAAAAD/////AgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQK2wAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQL5AAAWABQo3DTHwdFy0CCa+h6+bi7VJs3tcgAAAAAAAQDhAgAAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP////9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/////wIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUC+QAAFgAU/c11FQPC7OYlaspyx3uf70ntV8kBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUCzUAAFgAUZlkFHI0o4IUPP7h+AZ/85GpMC7QAAAAAAAEBQgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQLNQAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNldP8BAOECAAAAAAK2GUhaoOw0nV0BJhZg0a8G7vb9PQvlTds8GA/N+l+peAMAAAAA/////7YZSFqg7DSdXQEmFmDRrwbu9v09C+VN2zwYD836X6l4AQAAAAD/////AgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQK2wAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQL5AAAWABQo3DTHwdFy0CCa+h6+bi7VJs3tcgAAAAAAAQDhAgAAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP////9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/////wIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUC+QAAFgAU/c11FQPC7OYlaspyx3uf70ntV8kBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUCzUAAFgAUZlkFHI0o4IUPP7h+AZ/85GpMC7QAAAAAAAEBQgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQLNQAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
# Run all the pre-Elements, tests first with non-confidential addresses, then again with confidential addresses
self.run_basic_tests(False)
@ -726,7 +726,7 @@ class PSBTTest(BitcoinTestFramework):
assert_equal(analysis['next'], 'creator')
assert_equal(analysis['error'], 'PSBT is not valid. Input 0 specifies invalid prevout')
assert_raises_rpc_error(-25, 'Missing inputs', self.nodes[0].walletprocesspsbt, 'cHNldP8BAOECAAAAAAK2GUhaoOw0nV0BJhZg0a8G7vb9PQvlTds8GA/N+l+peAMAAAAA/////7YZSFqg7DSdXQEmFmDRrwbu9v09C+VN2zwYD836X6l4AQAAAAD/////AgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQK2wAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQL5AAAWABQo3DTHwdFy0CCa+h6+bi7VJs3tcgAAAAAAAQDhAgAAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP////9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/////wIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUC+QAAFgAU/c11FQPC7OYlaspyx3uf70ntV8kBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUCzUAAFgAUZlkFHI0o4IUPP7h+AZ/85GpMC7QAAAAAAAEBQgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQLNQAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
assert_raises_rpc_error(-25, 'Inputs missing or spent', self.nodes[0].walletprocesspsbt, 'cHNldP8BAOECAAAAAAK2GUhaoOw0nV0BJhZg0a8G7vb9PQvlTds8GA/N+l+peAMAAAAA/////7YZSFqg7DSdXQEmFmDRrwbu9v09C+VN2zwYD836X6l4AQAAAAD/////AgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQK2wAAWABSNJKzjaUb3uOxixsvh1GGE3fW7zQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQL5AAAWABQo3DTHwdFy0CCa+h6+bi7VJs3tcgAAAAAAAQDhAgAAAAACczMa321tVHuN4GKWKRncycI22aX3uXgwSFUKM2orjRsBAAAAAP////9zMxrfbW1Ue43gYpYpGdzJwjbZpfe5eDBIVQozaiuNGwAAAAAA/////wIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUC+QAAFgAU/c11FQPC7OYlaspyx3uf70ntV8kBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAJUCzUAAFgAUZlkFHI0o4IUPP7h+AZ/85GpMC7QAAAAAAAEBQgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAlQLNQAAWABRmWQUcjSjghQ8/uH4Bn/zkakwLtAAAAA==')
if __name__ == '__main__':
PSBTTest().main()

View file

@ -16,3 +16,4 @@ setban
hist
ser
unselect
lowercased