Skip to content

Commit ce2aeac

Browse files
feat(qt): register a DashPay username
A wallet's DashPay identity gets its username in the DashPay tab, as the mobile wallets do. UsernameFlow checks the label with the SDK's DPNS rule, refuses a contested (premium) label before anything is funded, and resolves it: a free label is registered with one registerName call, signed with the identity's enabled AUTHENTICATION/HIGH key, chosen by purpose and level rather than by id. A wallet without an identity creates it first through IdentityFlow, with the same exact-fee confirmation, and the identity and the name are signed under one passphrase. Nothing is persisted. The identity's names are read whenever it is found: the tab header shows the first one, and a name write that was dropped or failed is not sent again; the next read shows whether it landed. One write at a time per wallet holds across both flows through PlatformService::writing(): no top-up or Continue starts while a name write is in flight, and a free label meeting another write in flight is refused, since that write's unlock would relock the wallet under the name's. The unlock scope and the dropped-call token move to PlatformUi for both flows to share. FakePlatformClient records the identity and key of each registerName and can deliver one answer at a time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b53aaa0 commit ce2aeac

14 files changed

Lines changed: 680 additions & 48 deletions

‎src/Makefile.qt.include‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ if ENABLE_PLATFORM_GUI
126126
QT_MOC_CPP += \
127127
qt/platform/moc_identityflow.cpp \
128128
qt/platform/moc_platformpage.cpp \
129-
qt/platform/moc_platformservice.cpp
129+
qt/platform/moc_platformservice.cpp \
130+
qt/platform/moc_usernameflow.cpp
130131
endif
131132

132133
BITCOIN_MM = \
@@ -236,7 +237,8 @@ BITCOIN_QT_H += \
236237
qt/platform/identityflow.h \
237238
qt/platform/platformpage.h \
238239
qt/platform/platformservice.h \
239-
qt/platform/platformui.h
240+
qt/platform/platformui.h \
241+
qt/platform/usernameflow.h
240242
endif
241243

242244
QT_RES_ICONS = \
@@ -376,7 +378,8 @@ BITCOIN_QT_PLATFORM_CPP = \
376378
qt/platform/identityflow.cpp \
377379
qt/platform/platformpage.cpp \
378380
qt/platform/platformservice.cpp \
379-
qt/platform/platformui.cpp
381+
qt/platform/platformui.cpp \
382+
qt/platform/usernameflow.cpp
380383

381384
BITCOIN_QT_CPP = $(BITCOIN_QT_BASE_CPP)
382385
if TARGET_WINDOWS

‎src/qt/platform/identityflow.cpp‎

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ namespace {
2727
using platform::IdentityPublicKey;
2828
using Funding = IdentityFlow::Funding;
2929

30-
//! Every identity is index 0 until the UI offers more than one.
31-
constexpr uint32_t IDENTITY_INDEX{0};
3230
//! The asset lock's one credit output.
3331
constexpr uint32_t CREDIT_OUTPUT{0};
3432
//! rs-dpp IdentityAssetLockTransactionOutPointAlreadyConsumedError.
@@ -53,7 +51,10 @@ constexpr KeySpec REGISTRATION_KEYS[]{
5351
{5, IdentityPublicKey::Purpose::DECRYPTION, IdentityPublicKey::SecurityLevel::MEDIUM},
5452
};
5553

56-
std::string RegistrationRecord() { return strprintf("%sidentity/%u", IdentityFlow::FUNDING_PREFIX, IDENTITY_INDEX); }
54+
std::string RegistrationRecord()
55+
{
56+
return strprintf("%sidentity/%u", IdentityFlow::FUNDING_PREFIX, IdentityFlow::IDENTITY_INDEX);
57+
}
5758
std::string TopUpRecord(uint32_t index) { return strprintf("%stopup/%u", IdentityFlow::FUNDING_PREFIX, index); }
5859

5960
std::vector<unsigned char> Encode(const Funding& funding)
@@ -115,7 +116,8 @@ std::pair<CTransactionRef, interfaces::WalletTxStatus> WalletTxOf(interfaces::Wa
115116

116117
std::optional<std::array<uint8_t, 20>> MasterKeyHash(interfaces::Wallet& wallet)
117118
{
118-
const auto key{wallet.getPlatformPubKey(wallet::IdentityAuthKey{IDENTITY_INDEX, REGISTRATION_KEYS[0].id})};
119+
const auto key{
120+
wallet.getPlatformPubKey(wallet::IdentityAuthKey{IdentityFlow::IDENTITY_INDEX, REGISTRATION_KEYS[0].id})};
119121
if (!key) return std::nullopt;
120122
const CKeyID hash{key.value.GetID()};
121123
std::array<uint8_t, 20> out;
@@ -127,7 +129,7 @@ std::optional<std::vector<IdentityPublicKey>> RegistrationKeys(interfaces::Walle
127129
{
128130
std::vector<IdentityPublicKey> keys;
129131
for (const KeySpec& spec : REGISTRATION_KEYS) {
130-
const auto pubkey{wallet.getPlatformPubKey(wallet::IdentityAuthKey{IDENTITY_INDEX, spec.id})};
132+
const auto pubkey{wallet.getPlatformPubKey(wallet::IdentityAuthKey{IdentityFlow::IDENTITY_INDEX, spec.id})};
131133
if (!pubkey) return std::nullopt;
132134
IdentityPublicKey key;
133135
key.id = spec.id;
@@ -151,18 +153,6 @@ std::optional<platform::AssetLockProof> NodeProof(interfaces::Node& node, interf
151153
COutPoint{tx->GetHash(), CREDIT_OUTPUT}};
152154
}
153155

154-
//! Keeps the wallet unlocked until the client has signed the write.
155-
class WalletUnlock final : public platform::UnlockScope
156-
{
157-
public:
158-
explicit WalletUnlock(WalletModel::UnlockContext unlock) :
159-
m_unlock(std::move(unlock))
160-
{
161-
}
162-
163-
private:
164-
WalletModel::UnlockContext m_unlock;
165-
};
166156
} // namespace
167157

168158
IdentityFlow::IdentityFlow(PlatformService& service, Confirm confirm, Proof proof) :
@@ -189,14 +179,6 @@ std::optional<Funding> IdentityFlow::funding() const
189179
return std::nullopt;
190180
}
191181

192-
template <typename T>
193-
platform::PlatformClient::Callback<T> IdentityFlow::tracked(std::function<void(platform::Result<T>)> fn)
194-
{
195-
auto call{std::make_shared<int>(0)};
196-
m_call = call;
197-
return [call, fn = std::move(fn)](platform::Result<T> result) { fn(std::move(result)); };
198-
}
199-
200182
void IdentityFlow::serviceChanged()
201183
{
202184
// A call the client dropped never calls back; a funding record stays for Continue.
@@ -243,8 +225,9 @@ void IdentityFlow::startLookUp(bool unlock)
243225
if (m_step != Step::IDLE || !client) return;
244226
m_step = Step::LOOKING_UP;
245227
m_error.clear();
246-
client->getIdentityByPublicKeyHash(*m_key_hash, tracked<platform::Identity>(
247-
[this](auto result) { onLookedUp(std::move(result)); }));
228+
client->getIdentityByPublicKeyHash(*m_key_hash, PlatformUi::Tracked<platform::Identity>(m_call, [this](auto result) {
229+
onLookedUp(std::move(result));
230+
}));
248231
Q_EMIT changed();
249232
}
250233

@@ -311,7 +294,7 @@ void IdentityFlow::awaitLock(Funding pending, CTransactionRef tx, WalletModel::U
311294

312295
void IdentityFlow::fund(const wallet::AssetLockFundingKey& key, CAmount amount, Funding pending)
313296
{
314-
if (m_step != Step::IDLE || !m_service.client() || funding()) return;
297+
if (m_step != Step::IDLE || !m_service.client() || m_service.writing() || funding()) return;
315298
auto unlock{beginFunding()};
316299
if (!unlock) return;
317300
interfaces::Wallet& wallet{m_service.walletModel().wallet()};
@@ -338,7 +321,7 @@ void IdentityFlow::fund(const wallet::AssetLockFundingKey& key, CAmount amount,
338321
void IdentityFlow::resume()
339322
{
340323
auto pending{funding()};
341-
if (m_step != Step::IDLE || !m_service.client() || !pending) return;
324+
if (m_step != Step::IDLE || !m_service.client() || m_service.writing() || !pending) return;
342325
interfaces::Wallet& wallet{m_service.walletModel().wallet()};
343326
const auto [tx, status]{WalletTxOf(wallet, pending->txid)};
344327
if (!tx || status.is_abandoned || status.depth_in_main_chain < 0) {
@@ -365,20 +348,20 @@ void IdentityFlow::send()
365348
keys = RegistrationKeys(wallet);
366349
if (!keys) return finish(tr("Dash Core could not derive your identity's keys from this wallet."));
367350
}
368-
platform::Signing signing{wallet, IDENTITY_INDEX, std::make_unique<WalletUnlock>(std::move(*m_unlock))};
351+
platform::Signing signing{wallet, IDENTITY_INDEX, PlatformUi::KeepUnlocked(std::move(*m_unlock))};
369352
m_unlock.reset();
370353
m_step = Step::SENDING;
371354
if (m_funding->top_up) {
372355
client->topUpIdentity(m_funding->identity, *proof, *m_funding->top_up, std::move(signing),
373-
m_service.trackWrite<uint64_t>(
374-
tracked<uint64_t>([this, funding = *m_funding](platform::Result<uint64_t> result) {
356+
m_service.trackWrite<uint64_t>(PlatformUi::Tracked<uint64_t>(
357+
m_call, [this, funding = *m_funding](platform::Result<uint64_t> result) {
375358
if (result.ok() && m_identity) m_identity->balance = result.value;
376359
sent(funding, result.status);
377360
})));
378361
} else {
379362
client->registerIdentity(std::move(*keys), *proof, std::move(signing),
380-
m_service.trackWrite<platform::Identity>(tracked<platform::Identity>(
381-
[this, funding = *m_funding](platform::Result<platform::Identity> result) {
363+
m_service.trackWrite<platform::Identity>(PlatformUi::Tracked<platform::Identity>(
364+
m_call, [this, funding = *m_funding](platform::Result<platform::Identity> result) {
382365
if (result.ok()) {
383366
m_identity = std::move(result.value);
384367
m_looked_up = true;

‎src/qt/platform/identityflow.h‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ class IdentityFlow : public QObject
4646
Q_OBJECT
4747

4848
public:
49+
//! Every identity is index 0 until the UI offers more than one.
50+
static constexpr uint32_t IDENTITY_INDEX{0};
4951
//! What a new identity is funded with, as the mobile wallets: enough to
5052
//! register a username.
5153
static constexpr CAmount REGISTRATION_AMOUNT{3'000'000};
@@ -118,9 +120,6 @@ class IdentityFlow : public QObject
118120
void send();
119121
void sent(const Funding& funding, const platform::Status& status);
120122
void finish(const QString& error = {});
121-
//! The callback of the one call in flight; it expires if the call is dropped.
122-
template <typename T>
123-
platform::PlatformClient::Callback<T> tracked(std::function<void(platform::Result<T>)> fn);
124123

125124
PlatformService& m_service;
126125
const Confirm m_confirm;
@@ -132,6 +131,7 @@ class IdentityFlow : public QObject
132131
//! Hash160 of the MASTER key, kept once derived: deriving needs an
133132
//! unlocked wallet.
134133
std::optional<std::array<uint8_t, 20>> m_key_hash;
134+
//! The one call in flight, until it calls back or is dropped.
135135
std::weak_ptr<void> m_call;
136136
//! The funding being waited for or sent, and the unlock it signs with.
137137
std::optional<Funding> m_funding;

‎src/qt/platform/platformpage.cpp‎

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
#include <qt/optionsmodel.h>
1212
#include <qt/platform/identityflow.h>
1313
#include <qt/platform/platformui.h>
14+
#include <qt/platform/usernameflow.h>
1415
#include <qt/walletmodel.h>
1516

1617
#include <QHBoxLayout>
1718
#include <QLabel>
19+
#include <QLineEdit>
1820
#include <QMessageBox>
1921
#include <QPushButton>
2022
#include <QVBoxLayout>
@@ -26,6 +28,11 @@ PlatformPage::PlatformPage(PlatformService::Sources sources, QWidget* parent) :
2628
auto* layout{new QVBoxLayout(this)};
2729
auto* title{new QLabel(tr("DashPay"), this)};
2830
GUIUtil::setFont({title}, GUIUtil::FontWeight::Bold, 18);
31+
m_username = new QLabel(this);
32+
m_username->setObjectName("dashPayUsername");
33+
m_username->setTextFormat(Qt::PlainText);
34+
m_username->setTextInteractionFlags(Qt::TextSelectableByMouse);
35+
GUIUtil::setFont({m_username}, GUIUtil::FontWeight::Bold);
2936
m_identity = new QLabel(this);
3037
m_identity->setObjectName("dashPayIdentity");
3138
m_identity->setTextInteractionFlags(Qt::TextSelectableByMouse);
@@ -58,16 +65,36 @@ PlatformPage::PlatformPage(PlatformService::Sources sources, QWidget* parent) :
5865
actions->addWidget(m_top_up);
5966
actions->addStretch();
6067

68+
m_username_input = new QLineEdit(this);
69+
m_username_input->setObjectName("dashPayUsernameInput");
70+
m_username_input->setPlaceholderText(tr("Username"));
71+
m_register = new QPushButton(tr("Register username"), this);
72+
m_register->setObjectName("dashPayRegisterUsername");
73+
m_username_status = new QLabel(this);
74+
m_username_status->setObjectName("dashPayUsernameStatus");
75+
m_username_status->setWordWrap(true);
76+
auto* username_row{new QHBoxLayout};
77+
username_row->addWidget(m_username_input);
78+
username_row->addWidget(m_register);
79+
username_row->addStretch();
80+
81+
auto* header{new QHBoxLayout};
82+
header->addWidget(m_username);
83+
header->addWidget(m_identity);
84+
header->addStretch();
85+
6186
m_start = new QPushButton(tr("Start DashPay"), this);
6287
m_start->setObjectName("dashPayStart");
6388
m_stop = new QPushButton(tr("Turn off DashPay for this wallet"), this);
6489
m_stop->setObjectName("dashPayStop");
6590
layout->addWidget(title);
66-
layout->addWidget(m_identity);
91+
layout->addLayout(header);
6792
layout->addWidget(notice);
6893
layout->addWidget(m_status);
6994
layout->addWidget(m_identity_status);
7095
layout->addLayout(actions);
96+
layout->addWidget(m_username_status);
97+
layout->addLayout(username_row);
7198
layout->addWidget(m_start, 0, Qt::AlignLeft);
7299
layout->addWidget(m_stop, 0, Qt::AlignLeft);
73100
layout->addStretch();
@@ -86,6 +113,11 @@ PlatformPage::PlatformPage(PlatformService::Sources sources, QWidget* parent) :
86113
connect(m_look_up, &QPushButton::clicked, this, [this] { m_identity_flow->lookUp(); });
87114
connect(m_create, &QPushButton::clicked, this, [this] { m_identity_flow->create(); });
88115
connect(m_continue, &QPushButton::clicked, this, [this] { m_identity_flow->resume(); });
116+
const auto register_name = [this] {
117+
m_username_flow->registerName(m_username_input->text().trimmed().toStdString());
118+
};
119+
connect(m_register, &QPushButton::clicked, this, register_name);
120+
connect(m_username_input, &QLineEdit::returnPressed, this, register_name);
89121
connect(m_top_up, &QPushButton::clicked, this, [this] {
90122
bool valid{false};
91123
const CAmount amount{m_top_up_amount->value(&valid)};
@@ -117,6 +149,7 @@ void PlatformPage::resetService()
117149
{
118150
if (m_service) {
119151
Q_EMIT serviceChanged(nullptr);
152+
m_username_flow.reset();
120153
m_identity_flow.reset();
121154
m_service.reset();
122155
}
@@ -127,6 +160,8 @@ void PlatformPage::resetService()
127160
return confirmFunding(amount, fee);
128161
});
129162
connect(m_identity_flow.get(), &IdentityFlow::changed, this, &PlatformPage::refresh);
163+
m_username_flow = std::make_unique<UsernameFlow>(*m_service, *m_identity_flow);
164+
connect(m_username_flow.get(), &UsernameFlow::changed, this, &PlatformPage::refresh);
130165
Q_EMIT serviceChanged(m_service.get());
131166
}
132167
refresh();
@@ -167,11 +202,17 @@ void PlatformPage::refresh()
167202
PlatformUi::FormatCredits(unit, identity->balance)));
168203
}
169204
m_identity->setVisible(identity != nullptr);
205+
const UsernameFlow* names{m_username_flow.get()};
206+
const auto username{names && identity ? names->username() : std::nullopt};
207+
m_username->setText(QString::fromStdString(username.value_or(std::string{})));
208+
m_username->setVisible(username.has_value());
170209

171210
using Step = IdentityFlow::Step;
172211
const Step step{flow ? flow->step() : Step::IDLE};
173212
const auto funding{running ? flow->funding() : std::nullopt};
174-
const bool idle{running && step == Step::IDLE};
213+
using NameStep = UsernameFlow::Step;
214+
const NameStep name_step{names ? names->step() : NameStep::IDLE};
215+
const bool idle{running && step == Step::IDLE && name_step == NameStep::IDLE};
175216
//! Nothing in flight and no funding waiting for Platform.
176217
const bool ready{idle && !funding};
177218
QString identity_status;
@@ -211,4 +252,26 @@ void PlatformPage::refresh()
211252
m_continue->setVisible(idle && funding.has_value());
212253
m_top_up_amount->setVisible(ready && identity);
213254
m_top_up->setVisible(ready && identity);
255+
256+
QString username_status;
257+
switch (name_step) {
258+
case NameStep::IDLE:
259+
if (running) username_status = names->error();
260+
break;
261+
case NameStep::CHECKING:
262+
username_status = tr("Checking that the username is free…");
263+
break;
264+
case NameStep::PREPARING:
265+
// Until the identity exists its own status says what runs.
266+
if (identity) username_status = tr("Registering your username…");
267+
break;
268+
case NameStep::REGISTERING:
269+
username_status = tr("Registering your username…");
270+
break;
271+
}
272+
m_username_status->setText(username_status);
273+
m_username_status->setVisible(!username_status.isEmpty());
274+
const bool can_register{ready && flow->lookedUp() && (!identity || (names->namesRead() && !username))};
275+
m_username_input->setVisible(can_register);
276+
m_register->setVisible(can_register);
214277
}

‎src/qt/platform/platformpage.h‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414

1515
class BitcoinAmountField;
1616
class IdentityFlow;
17+
class UsernameFlow;
1718

1819
QT_BEGIN_NAMESPACE
1920
class QLabel;
21+
class QLineEdit;
2022
class QPushButton;
2123
QT_END_NAMESPACE
2224

@@ -25,7 +27,8 @@ QT_END_NAMESPACE
2527
* It owns the wallet's PlatformService while both models are set, offers
2628
* the first DashPay click with a one-line privacy notice, and says why
2729
* DashPay can't run when a gate is closed. Once DashPay runs it shows the
28-
* wallet's identity and its balance, and creates, continues or tops it up. */
30+
* wallet's identity, its username and its balance; creates, continues or
31+
* tops up the identity; and registers its username. */
2932
class PlatformPage : public QWidget
3033
{
3134
Q_OBJECT
@@ -38,6 +41,7 @@ class PlatformPage : public QWidget
3841
void setClientModel(ClientModel* client_model);
3942
PlatformService* service() const { return m_service.get(); }
4043
IdentityFlow* identityFlow() const { return m_identity_flow.get(); }
44+
UsernameFlow* usernameFlow() const { return m_username_flow.get(); }
4145

4246
Q_SIGNALS:
4347
//! A service was created, or is about to be destroyed (nullptr) while
@@ -55,7 +59,10 @@ class PlatformPage : public QWidget
5559
std::unique_ptr<PlatformService> m_service;
5660
//! Destroyed before the service it calls through.
5761
std::unique_ptr<IdentityFlow> m_identity_flow;
62+
//! Destroyed before the identity flow it builds on.
63+
std::unique_ptr<UsernameFlow> m_username_flow;
5864

65+
QLabel* m_username{nullptr};
5966
QLabel* m_identity{nullptr};
6067
QLabel* m_status{nullptr};
6168
QLabel* m_identity_status{nullptr};
@@ -64,6 +71,9 @@ class PlatformPage : public QWidget
6471
QPushButton* m_continue{nullptr};
6572
BitcoinAmountField* m_top_up_amount{nullptr};
6673
QPushButton* m_top_up{nullptr};
74+
QLineEdit* m_username_input{nullptr};
75+
QPushButton* m_register{nullptr};
76+
QLabel* m_username_status{nullptr};
6777
QPushButton* m_start{nullptr};
6878
QPushButton* m_stop{nullptr};
6979
};

‎src/qt/platform/platformservice.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ class PlatformService : public QObject
115115
//! rebuild waits for a write. A call whose client is destroyed never
116116
//! calls back.
117117
platform::PlatformClient* client() const { return m_paused ? nullptr : m_client.get(); }
118+
//! Whether a write is in flight. The wallet's flows start one only when
119+
//! none is, which serializes the wallet's unlocks and its identity's
120+
//! state transitions.
121+
bool writing() const { return m_writes > 0; }
118122

119123
//! Wrap the callback of a write, in the call that starts it: a rebuild
120124
//! waits until it has run.

0 commit comments

Comments
 (0)