Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/qt/digibytegui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,8 @@ void DigiByteGUI::createActions()
backupWalletAction->setStatusTip(tr("Backup wallet to another location"));
changePassphraseAction = new QAction(tr("&Change Passphrase…"), this);
changePassphraseAction->setStatusTip(tr("Change the passphrase used for wallet encryption"));
unlockWalletAction = new QAction(tr("&Unlock Wallet…"), this);
unlockWalletAction->setStatusTip(tr("Unlock the wallet so it can sign transactions"));
signMessageAction = new QAction(tr("Sign &message…"), this);
signMessageAction->setStatusTip(tr("Sign messages with your DigiByte addresses to prove you own them"));
verifyMessageAction = new QAction(tr("&Verify message…"), this);
Expand Down Expand Up @@ -520,6 +522,7 @@ void DigiByteGUI::createActions()
connect(encryptWalletAction, &QAction::triggered, walletFrame, &WalletFrame::encryptWallet);
connect(backupWalletAction, &QAction::triggered, walletFrame, &WalletFrame::backupWallet);
connect(changePassphraseAction, &QAction::triggered, walletFrame, &WalletFrame::changePassphrase);
connect(unlockWalletAction, &QAction::triggered, walletFrame, &WalletFrame::unlockWalletFromMenu);
connect(signMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
connect(signMessageAction, &QAction::triggered, [this]{ gotoSignMessageTab(); });
connect(m_load_psbt_action, &QAction::triggered, [this]{ gotoLoadPSBT(); });
Expand Down Expand Up @@ -636,6 +639,7 @@ void DigiByteGUI::createMenuBar()
{
settings->addAction(encryptWalletAction);
settings->addAction(changePassphraseAction);
settings->addAction(unlockWalletAction);
settings->addSeparator();
settings->addAction(m_mask_values_action);
settings->addSeparator();
Expand Down Expand Up @@ -967,6 +971,8 @@ void DigiByteGUI::setWalletActionsEnabled(bool enabled)
encryptWalletAction->setEnabled(enabled);
backupWalletAction->setEnabled(enabled);
changePassphraseAction->setEnabled(enabled);
// setEncryptionStatus() refines this to the Locked case only.
unlockWalletAction->setEnabled(enabled);
signMessageAction->setEnabled(enabled);
verifyMessageAction->setEnabled(enabled);
usedSendingAddressesAction->setEnabled(enabled);
Expand Down Expand Up @@ -1640,12 +1646,14 @@ void DigiByteGUI::setEncryptionStatus(int status)
encryptWalletAction->setChecked(false);
changePassphraseAction->setEnabled(false);
encryptWalletAction->setEnabled(false);
unlockWalletAction->setEnabled(false);
break;
case WalletModel::Unencrypted:
labelWalletEncryptionIcon->hide();
encryptWalletAction->setChecked(false);
changePassphraseAction->setEnabled(false);
encryptWalletAction->setEnabled(true);
unlockWalletAction->setEnabled(false);
break;
case WalletModel::Unlocked:
labelWalletEncryptionIcon->show();
Expand All @@ -1654,6 +1662,7 @@ void DigiByteGUI::setEncryptionStatus(int status)
encryptWalletAction->setChecked(true);
changePassphraseAction->setEnabled(true);
encryptWalletAction->setEnabled(false);
unlockWalletAction->setEnabled(false);
break;
case WalletModel::Locked:
labelWalletEncryptionIcon->show();
Expand All @@ -1662,6 +1671,7 @@ void DigiByteGUI::setEncryptionStatus(int status)
encryptWalletAction->setChecked(true);
changePassphraseAction->setEnabled(true);
encryptWalletAction->setEnabled(false);
unlockWalletAction->setEnabled(true);
break;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/qt/digibytegui.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ class DigiByteGUI : public QMainWindow
QAction* encryptWalletAction = nullptr;
QAction* backupWalletAction = nullptr;
QAction* changePassphraseAction = nullptr;
QAction* unlockWalletAction = nullptr;
QAction* aboutQtAction = nullptr;
QAction* openRPCConsoleAction = nullptr;
QAction* openAction = nullptr;
Expand Down
42 changes: 24 additions & 18 deletions src/qt/digidollarpositionswidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -465,9 +465,10 @@ void DigiDollarPositionsWidget::loadPositionsFromWallet()
// Get current oracle price in micro-USD; regtest can use mock helpers,
// while testnet/mainnet use the live oracle price.
CAmount oraclePrice = GetMockOraclePrice();
const bool isWatchOnly = m_walletModel->wallet().privateKeysDisabled();
const bool isWalletLocked = m_walletModel->getEncryptionStatus() == WalletModel::Locked;
const bool walletCanSign = !isWatchOnly && !isWalletLocked;
// Only a watch-only wallet can never sign a redemption. A locked but
// encrypted wallet still holds its keys and the redeem flow prompts for the
// passphrase, so its vaults stay redeemable.
const bool walletCanSign = !m_walletModel->wallet().privateKeysDisabled();

// Get positions from wallet backend
std::vector<WalletCollateralPosition> walletPositions = GetWalletPositions();
Expand Down Expand Up @@ -515,7 +516,7 @@ void DigiDollarPositionsWidget::loadPositionsFromWallet()
pos.health = CalculatePositionHealth(wp.dd_minted, wp.dgb_collateral, oraclePrice);

// Can redeem if timelock expired, position is active, and the wallet can sign.
// Private-key-disabled/watch-only wallets may observe vaults but cannot unlock them.
// Private-key-disabled/watch-only wallets may observe vaults but can never redeem them.
pos.canRedeem = walletCanSign && (pos.blocksRemaining == 0) && wp.is_active;

// A freshly-created mint can be known to the wallet before the collateral
Expand Down Expand Up @@ -856,8 +857,8 @@ QPushButton* DigiDollarPositionsWidget::createRedeemButton(const QString& positi
// - "Redeemed" if already redeemed (with strikethrough)
// - "Watch-Only" if the wallet has private keys disabled and so cannot
// ever construct a redemption witness
// - "Wallet Locked" if private keys exist but are currently unavailable
// - "Redeem" if can redeem now (green, clickable)
// - "Redeem" if can redeem now (clickable; a locked wallet is prompted for
// its passphrase by the redeem flow, so it stays clickable too)
// - "Locked" if vault hasn't matured yet (grayed out)
QString buttonText;
if (isPendingMint) {
Expand All @@ -868,8 +869,6 @@ QPushButton* DigiDollarPositionsWidget::createRedeemButton(const QString& positi
buttonText = tr("Redeemed");
} else if (isWatchOnly) {
buttonText = tr("Watch-Only");
} else if (isWalletLocked) {
buttonText = tr("Wallet Locked");
} else if (canRedeem) {
buttonText = tr("Redeem");
} else {
Expand Down Expand Up @@ -967,25 +966,30 @@ QPushButton* DigiDollarPositionsWidget::createRedeemButton(const QString& positi
.arg(woText);
tooltip = tr("Watch-only wallet\nThis wallet cannot sign DigiDollar redemptions because private keys are disabled.");
button->setEnabled(false);
} else if (isWalletLocked) {
} else if (canRedeem && isWalletLocked) {
// Redeemable, but the wallet is locked: keep the action available and
// use a distinct styling to flag the extra passphrase step.
QString lockedWalletBg = isDarkTheme ? "#4a4655" : "#e4dfea";
QString lockedWalletText = isDarkTheme ? "#d6c6e6" : "#4d3f5f";

// Metrics match the plain redeemable branch below so that the same
// "Redeem" label does not change size with the wallet lock state; only
// the colour differs.
buttonStyle = QString(
"QPushButton { "
" background-color: %1; "
" color: %2; "
" border: 1px solid %2; "
" border-radius: 5px; "
" padding: 6px 8px; "
" padding: 6px 12px; "
" font-weight: 600; "
" font-size: 10px; "
" min-width: 72px; "
" font-size: 11px; "
" min-width: 60px; "
"}")
.arg(lockedWalletBg)
.arg(lockedWalletText);
tooltip = tr("Wallet is locked\nUnlock the wallet to redeem this DigiDollar vault.");
button->setEnabled(false);
tooltip = tr("Wallet is locked\nYou will be asked for your passphrase before this DigiDollar vault is redeemed.");
button->setEnabled(true);
} else if (canRedeem) {
// Can redeem - green button
QString successColor = isDarkTheme ? "#4caf50" : "#28a745";
Expand Down Expand Up @@ -1242,10 +1246,12 @@ std::vector<WalletCollateralPosition> DigiDollarPositionsWidget::GetWalletPositi
// Access DigiDollarWallet directly from wallet model
DigiDollarWallet* ddWallet = m_walletModel->wallet().getDigiDollarWallet();
if (ddWallet) {
const bool walletCannotSign =
m_walletModel->wallet().privateKeysDisabled() ||
m_walletModel->getEncryptionStatus() == WalletModel::Locked;
if (!walletCannotSign) {
// ReconcilePositionStates() only reads the wallet's transactions and the
// chain UTXO set and rewrites the is_active flag; it never touches key
// material, so a locked wallet must still refresh. Skipping it there
// would leave the row action enabled off a stale is_active flag.
const bool walletIsWatchOnly = m_walletModel->wallet().privateKeysDisabled();
if (!walletIsWatchOnly) {
ddWallet->ReconcilePositionStates();
}
positions = ddWallet->GetDDTimeLocks(false); // Get ALL time locks including redeemed ones
Expand Down
41 changes: 28 additions & 13 deletions src/qt/digidollarredeemwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -753,12 +753,20 @@ void DigiDollarRedeemWidget::updateRedeemButtons()
validateDDBalance() &&
canWalletSignRedemption();

// A locked wallet keeps the action available: the redeem flow prompts for
// the passphrase. Only advertise the extra step up front.
const bool needsUnlock = canRedeem && walletNeedsUnlockToRedeem();

m_redeemButton->setEnabled(canRedeem);
if (canRedeem) {
m_redeemButton->setText(tr("Redeem && Unlock DGB"));
const QString readyText = tr("Ready to redeem this DigiDollar vault and release the locked DGB collateral.");
m_redeemButton->setText(needsUnlock ? tr("Unlock && Redeem") : tr("Redeem && Unlock DGB"));
const QString readyText = needsUnlock
? tr("Wallet is locked.\nYou will be asked for your passphrase, then this DigiDollar vault is redeemed and the locked DGB collateral released.")
: tr("Ready to redeem this DigiDollar vault and release the locked DGB collateral.");
m_redeemButton->setToolTip(readyText);
m_positionValidationLabel->setText(tr("Vault ready to redeem."));
m_positionValidationLabel->setText(needsUnlock
? tr("Vault ready to redeem — you will be asked for your passphrase.")
: tr("Vault ready to redeem."));
m_positionValidationLabel->setToolTip(readyText);
} else {
const QString reason = redeemDisabledReason();
Expand Down Expand Up @@ -838,10 +846,11 @@ void DigiDollarRedeemWidget::loadPositionDetails()
auto loadPositionFromWallet = [&]() -> bool {
DigiDollarWallet* ddWallet = m_walletModel->wallet().getDigiDollarWallet();
if (!ddWallet) return false;
const bool walletCannotSign =
m_walletModel->wallet().privateKeysDisabled() ||
m_walletModel->getEncryptionStatus() == WalletModel::Locked;
if (!walletCannotSign) {
// ReconcilePositionStates() only reads the wallet's transactions and the
// chain UTXO set and rewrites the is_active flag; it never touches key
// material, so a locked wallet must still refresh.
const bool walletIsWatchOnly = m_walletModel->wallet().privateKeysDisabled();
if (!walletIsWatchOnly) {
ddWallet->ReconcilePositionStates();
}

Expand Down Expand Up @@ -1014,15 +1023,24 @@ bool DigiDollarRedeemWidget::canWalletSignRedemption() const
if (!m_walletModel) {
return false;
}
// Private keys disabled (watch-only) is a permanent incapability: no
// passphrase can make this wallet produce a redemption witness.
//
// A locked but encrypted wallet is NOT in that category. It still holds the
// keys, and onRedeemClicked() asks for the passphrase through
// WalletModel::requestUnlock() before building the transaction, exactly
// like the send and mint flows do.
if (m_walletModel->wallet().privateKeysDisabled()) {
return false;
}
if (m_walletModel->getEncryptionStatus() == WalletModel::Locked) {
return false;
}
return true;
}

bool DigiDollarRedeemWidget::walletNeedsUnlockToRedeem() const
{
return m_walletModel && m_walletModel->getEncryptionStatus() == WalletModel::Locked;
}

QString DigiDollarRedeemWidget::redeemDisabledReason() const
{
if (!m_positionFound) {
Expand All @@ -1039,9 +1057,6 @@ QString DigiDollarRedeemWidget::redeemDisabledReason() const
if (m_walletModel->wallet().privateKeysDisabled()) {
return tr("Watch-only wallet.\nThis wallet cannot sign DigiDollar redemptions because private keys are disabled.");
}
if (m_walletModel->getEncryptionStatus() == WalletModel::Locked) {
return tr("Wallet is locked.\nUnlock the wallet to redeem this DigiDollar vault.");
}
if (!validateAmount()) {
return tr("Invalid redeem amount.\nDigiDollar redemptions must use the exact vault amount.");
}
Expand Down
5 changes: 5 additions & 0 deletions src/qt/digidollarredeemwidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@ private Q_SLOTS:
bool validateAmount() const;
bool validateRedeemable() const;
bool validateDDBalance() const;
/** True unless the wallet can never sign a redemption (no wallet model, or
* private keys disabled). A locked but encrypted wallet still holds its
* keys, so it returns true; see walletNeedsUnlockToRedeem(). */
bool canWalletSignRedemption() const;
/** True when redeeming will first prompt for the wallet passphrase. */
bool walletNeedsUnlockToRedeem() const;
QString redeemDisabledReason() const;

QString formatDDAmount(double amount) const;
Expand Down
Loading
Loading