feat(wallet): implement asynchronous connection and snapshot handling

This commit is contained in:
Ricardo Guilherme Schmidt 2026-07-17 14:09:11 -03:00
parent 1692581f60
commit 90ba65f2d0
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
10 changed files with 574 additions and 72 deletions

View File

@ -12,9 +12,11 @@ Popup {
property var snapshot: ({}) property var snapshot: ({})
property Component summary: null property Component summary: null
property bool confirmationPending: false property bool confirmationPending: false
property bool confirmEnabled: true
signal canceled signal canceled
signal confirmed(var snapshot) signal confirmed(var snapshot)
signal summaryEdited(var snapshot)
modal: true modal: true
dim: true dim: true
@ -43,6 +45,10 @@ Popup {
cancelButton.forceActiveFocus() cancelButton.forceActiveFocus()
} }
function updateSnapshot(nextSnapshot) {
root.snapshot = root.cloneSnapshot(nextSnapshot)
}
function cancel() { function cancel() {
if (root.busy) if (root.busy)
return return
@ -52,7 +58,7 @@ Popup {
} }
function confirm() { function confirm() {
if (root.busy) if (root.busy || !root.confirmEnabled)
return return
root.confirmationPending = true root.confirmationPending = true
root.confirmed(root.snapshot) root.confirmed(root.snapshot)
@ -62,6 +68,16 @@ Popup {
} }
} }
Connections {
target: summaryLoader.item
ignoreUnknownSignals: true
function onSnapshotEdited(snapshot) {
root.updateSnapshot(snapshot)
root.summaryEdited(root.snapshot)
}
}
onBusyChanged: { onBusyChanged: {
if (!root.busy && root.confirmationPending) { if (!root.busy && root.confirmationPending) {
root.confirmationPending = false root.confirmationPending = false
@ -145,7 +161,7 @@ Popup {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 44 implicitHeight: 44
text: root.busy ? qsTr("Submitting...") : root.confirmText text: root.busy ? qsTr("Submitting...") : root.confirmText
enabled: !root.busy enabled: !root.busy && root.confirmEnabled
Accessible.name: text Accessible.name: text
onClicked: root.confirm() onClicked: root.confirm()

View File

@ -16,6 +16,11 @@ Item {
property bool busy: false property bool busy: false
readonly property bool connected: root.wallet !== null && root.wallet.isWalletOpen readonly property bool connected: root.wallet !== null && root.wallet.isWalletOpen
readonly property string syncStatus: root.wallet
? String(root.wallet.walletSyncStatus || "closed")
: "closed"
readonly property bool syncing: root.syncStatus === "opening"
|| root.syncStatus === "syncing"
readonly property bool compactLayout: root.compact || root.viewportWidth < 680 readonly property bool compactLayout: root.compact || root.viewportWidth < 680
readonly property string selectedAddress: root.accountAt(root.selectedIndex, "address") readonly property string selectedAddress: root.accountAt(root.selectedIndex, "address")
readonly property string selectedName: root.accountAt(root.selectedIndex, "name") readonly property string selectedName: root.accountAt(root.selectedIndex, "name")
@ -129,10 +134,13 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: !root.connected visible: !root.connected
enabled: root.wallet !== null && !root.busy enabled: root.wallet !== null && !root.busy && !root.syncing
implicitHeight: 40 implicitHeight: 40
implicitWidth: root.compactLayout ? 40 : 108 implicitWidth: root.compactLayout ? 40 : 108
text: root.compactLayout ? "" : root.busy ? qsTr("Connecting...") : qsTr("Connect") text: root.compactLayout ? ""
: root.syncStatus === "opening" ? qsTr("Opening...")
: root.syncStatus === "syncing" ? qsTr("Syncing...")
: root.busy ? qsTr("Connecting...") : qsTr("Connect")
display: root.compactLayout ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon display: root.compactLayout ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
icon.source: Qt.resolvedUrl("icons/account.svg") icon.source: Qt.resolvedUrl("icons/account.svg")
icon.color: "#ffffff" icon.color: "#ffffff"
@ -198,7 +206,7 @@ Item {
Layout.preferredWidth: 8 Layout.preferredWidth: 8
Layout.preferredHeight: 8 Layout.preferredHeight: 8
radius: 4 radius: 4
color: "#22c55e" color: root.syncing ? "#f59e0b" : "#22c55e"
} }
Label { Label {

View File

@ -1,10 +1,13 @@
#include "LogosWalletProvider.h" #include "LogosWalletProvider.h"
#include <algorithm>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonParseError> #include <QJsonParseError>
#include <QTimer>
#include <QVariantList> #include <QVariantList>
#include <QVariantMap> #include <QVariantMap>
@ -73,6 +76,39 @@ WalletCreation failedCreation(WalletFailure failure)
creation.snapshot.failure = failure; creation.snapshot.failure = failure;
return creation; return creation;
} }
WalletAccountRead parsePublicAccount(const QString& accountId, const QString& payload)
{
WalletAccountRead read;
read.accountId = accountId;
if (!isHex(accountId, 64))
return read;
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(payload.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject())
return read;
const QJsonObject account = document.object();
const QString owner = account.value(QStringLiteral("program_owner")).toString();
const QString balance = account.value(QStringLiteral("balance")).toString();
const QString nonce = account.value(QStringLiteral("nonce")).toString();
const QString data = account.value(QStringLiteral("data")).toString();
if (!isHex(owner, 64)
|| !isHex(balance, 32)
|| !isHex(nonce, 32)
|| data.size() % 2 != 0
|| !isHex(data, data.size())) {
return read;
}
read.status = QStringLiteral("ok");
read.programOwner = owner;
read.balanceHex = balance;
read.nonceHex = nonce;
read.dataHex = data;
return read;
}
} }
struct LogosWalletProvider::Impl { struct LogosWalletProvider::Impl {
@ -130,6 +166,76 @@ WalletSession LogosWalletProvider::connect(const WalletPaths& paths)
return session; return session;
} }
void LogosWalletProvider::connectAsync(const WalletPaths& paths, SessionCallback callback)
{
clearSnapshot();
const quint64 generation = ++m_generation;
if (!m_impl->logos) {
QTimer::singleShot(0, [callback = std::move(callback)]() mutable {
callback(failedSession(WalletFailure::WalletUnavailable));
});
return;
}
auto finishOpen = [this, generation, callback = std::move(callback)](
bool adopted, WalletFailure failure) mutable {
if (generation != m_generation)
return;
if (failure != WalletFailure::None) {
callback(failedSession(failure));
return;
}
m_connected = true;
loadSnapshotAsync(generation,
[this, generation, adopted, callback = std::move(callback)](
WalletSnapshot snapshot) mutable {
if (generation != m_generation)
return;
WalletSession session;
session.adopted = adopted;
session.failure = snapshot.failure;
session.snapshot = std::move(snapshot);
callback(std::move(session));
});
};
auto openStored = [this, generation, paths, finishOpen]() mutable {
if (generation != m_generation)
return;
if (!QFileInfo::exists(paths.storage)) {
finishOpen(false, WalletFailure::WalletMissing);
return;
}
m_impl->logos->logos_execution_zone.openAsync(
paths.config, paths.storage,
[this, generation, finishOpen](int result) mutable {
if (generation != m_generation)
return;
finishOpen(false, result == WALLET_FFI_SUCCESS
? WalletFailure::None : WalletFailure::OpenFailed);
});
};
m_impl->logos->logos_execution_zone.get_sequencer_addrAsync(
[this, generation, finishOpen, openStored](QString address) mutable {
if (generation != m_generation)
return;
if (!address.isEmpty()) {
finishOpen(true, WalletFailure::None);
return;
}
m_impl->logos->logos_execution_zone.list_accountsAsync(
[this, generation, finishOpen, openStored](QVariantList accounts) mutable {
if (generation != m_generation)
return;
if (!accounts.isEmpty())
finishOpen(true, WalletFailure::None);
else
openStored();
});
});
}
WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths, WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths,
const QString& password) const QString& password)
{ {
@ -180,6 +286,26 @@ WalletSnapshot LogosWalletProvider::snapshot(bool forceRefresh)
return result; return result;
} }
void LogosWalletProvider::snapshotAsync(bool forceRefresh, SnapshotCallback callback)
{
if (m_snapshotReady && !forceRefresh) {
const WalletSnapshot snapshot = m_snapshot;
QTimer::singleShot(0, [callback = std::move(callback), snapshot]() mutable {
callback(snapshot);
});
return;
}
if (!m_connected) {
WalletSnapshot snapshot;
snapshot.failure = WalletFailure::WalletUnavailable;
QTimer::singleShot(0, [callback = std::move(callback), snapshot]() mutable {
callback(snapshot);
});
return;
}
loadSnapshotAsync(++m_generation, std::move(callback));
}
void LogosWalletProvider::clearSnapshot() void LogosWalletProvider::clearSnapshot()
{ {
m_snapshot = {}; m_snapshot = {};
@ -208,45 +334,47 @@ WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic)
if (isPublic) if (isPublic)
creation.publicAccount = readPublicAccount(creation.accountId); creation.publicAccount = readPublicAccount(creation.accountId);
if (m_snapshotReady) {
clearSnapshot(); WalletAccount account;
creation.snapshot = snapshot(true); account.address = creation.accountId;
account.isPublic = isPublic;
if (isPublic && creation.publicAccount.ok()) {
account.balance = littleEndianU128ToDecimal(creation.publicAccount.balanceHex);
auto read = std::find_if(
m_snapshot.publicAccountReads.begin(),
m_snapshot.publicAccountReads.end(),
[&creation](const WalletAccountRead& existing) {
return existing.accountId == creation.accountId;
});
if (read == m_snapshot.publicAccountReads.end())
m_snapshot.publicAccountReads.append(creation.publicAccount);
else
*read = creation.publicAccount;
} else {
account.balance = m_impl->logos->logos_execution_zone.get_balance(
creation.accountId, isPublic);
}
auto existing = std::find_if(
m_snapshot.accounts.begin(), m_snapshot.accounts.end(),
[&creation](const WalletAccount& candidate) {
return candidate.address == creation.accountId;
});
if (existing == m_snapshot.accounts.end())
m_snapshot.accounts.append(account);
else
*existing = account;
creation.snapshot = m_snapshot;
}
return creation; return creation;
} }
WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const
{ {
WalletAccountRead read;
read.accountId = accountId;
if (!m_impl->logos || !isHex(accountId, 64)) if (!m_impl->logos || !isHex(accountId, 64))
return read; return WalletAccountRead { accountId };
return parsePublicAccount(
QJsonParseError parseError; accountId,
const QJsonDocument document = QJsonDocument::fromJson( m_impl->logos->logos_execution_zone.get_account_public(accountId));
m_impl->logos->logos_execution_zone.get_account_public(accountId).toUtf8(),
&parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject())
return read;
const QJsonObject account = document.object();
const QString owner = account.value(QStringLiteral("program_owner")).toString();
const QString balance = account.value(QStringLiteral("balance")).toString();
const QString nonce = account.value(QStringLiteral("nonce")).toString();
const QString data = account.value(QStringLiteral("data")).toString();
if (!isHex(owner, 64)
|| !isHex(balance, 32)
|| !isHex(nonce, 32)
|| data.size() % 2 != 0
|| !isHex(data, data.size())) {
return read;
}
read.status = QStringLiteral("ok");
read.programOwner = owner;
read.balanceHex = balance;
read.nonceHex = nonce;
read.dataHex = data;
return read;
} }
WalletSubmission LogosWalletProvider::submitPublicTransaction( WalletSubmission LogosWalletProvider::submitPublicTransaction(
@ -314,6 +442,7 @@ WalletSubmission LogosWalletProvider::submitPublicTransaction(
void LogosWalletProvider::disconnect() void LogosWalletProvider::disconnect()
{ {
++m_generation;
if (m_connected) if (m_connected)
save(); save();
clearSnapshot(); clearSnapshot();
@ -375,6 +504,161 @@ WalletSnapshot LogosWalletProvider::loadSnapshot()
return result; return result;
} }
void LogosWalletProvider::loadSnapshotAsync(quint64 generation, SnapshotCallback callback)
{
if (!m_impl->logos || generation != m_generation)
return;
m_impl->logos->logos_execution_zone.get_current_block_heightAsync(
[this, generation, callback = std::move(callback)](int currentHeight) mutable {
if (generation != m_generation)
return;
auto afterSync = [this, generation, currentHeight,
callback = std::move(callback)](int syncResult) mutable {
if (generation != m_generation)
return;
if (syncResult != WALLET_FFI_SUCCESS) {
WalletSnapshot failed;
failed.failure = WalletFailure::ReadFailed;
callback(std::move(failed));
return;
}
m_impl->logos->logos_execution_zone.get_last_synced_blockAsync(
[this, generation, currentHeight,
callback = std::move(callback)](int lastSynced) mutable {
if (generation != m_generation)
return;
m_impl->logos->logos_execution_zone.get_sequencer_addrAsync(
[this, generation, currentHeight, lastSynced,
callback = std::move(callback)](QString address) mutable {
if (generation != m_generation)
return;
m_impl->logos->logos_execution_zone.list_accountsAsync(
[this, generation, currentHeight, lastSynced,
address = std::move(address),
callback = std::move(callback)](
QVariantList entries) mutable {
if (generation != m_generation)
return;
struct SnapshotState {
WalletSnapshot snapshot;
QVector<WalletAccountRead> publicReads;
QVector<bool> publicFlags;
qsizetype remaining = 0;
SnapshotCallback callback;
};
auto state = std::make_shared<SnapshotState>();
state->snapshot.currentBlockHeight = static_cast<quint64>(
qMax(0, currentHeight));
state->snapshot.lastSyncedBlock = static_cast<quint64>(
qMax(0, lastSynced));
state->snapshot.sequencerAddress = std::move(address);
state->snapshot.accounts.resize(entries.size());
state->publicReads.resize(entries.size());
state->publicFlags.resize(entries.size());
state->remaining = entries.size();
state->callback = std::move(callback);
for (qsizetype index = 0; index < entries.size(); ++index) {
const QVariantMap entry = entries.at(index).toMap();
const QString accountId = entry
.value(QStringLiteral("account_id")).toString();
if (entry.isEmpty() || !isHex(accountId, 64)) {
state->snapshot.failure = WalletFailure::ReadFailed;
state->callback(std::move(state->snapshot));
return;
}
state->snapshot.accounts[index] = WalletAccount {
accountId,
{},
entry.value(QStringLiteral("is_public"), true).toBool(),
};
state->publicFlags[index] =
state->snapshot.accounts.at(index).isPublic;
}
auto finishOne = std::make_shared<std::function<void()>>();
*finishOne = [this, generation, state, finishOne]() mutable {
if (generation != m_generation || --state->remaining > 0)
return;
for (qsizetype index = 0;
index < state->publicReads.size(); ++index) {
if (state->publicFlags.at(index))
state->snapshot.publicAccountReads.append(
state->publicReads.at(index));
}
m_impl->logos->logos_execution_zone.saveAsync(
[this, generation, state](int result) mutable {
if (generation != m_generation)
return;
if (result != WALLET_FFI_SUCCESS)
state->snapshot.failure = WalletFailure::SaveFailed;
if (state->snapshot.ok()) {
m_snapshot = state->snapshot;
m_snapshotReady = true;
}
state->callback(std::move(state->snapshot));
});
};
if (entries.isEmpty()) {
state->remaining = 1;
(*finishOne)();
return;
}
for (qsizetype index = 0; index < entries.size(); ++index) {
const WalletAccount account = state->snapshot.accounts.at(index);
if (!account.isPublic) {
m_impl->logos->logos_execution_zone.get_balanceAsync(
account.address, false,
[state, finishOne, index](QString balance) {
state->snapshot.accounts[index].balance =
std::move(balance);
(*finishOne)();
});
continue;
}
m_impl->logos->logos_execution_zone.get_account_publicAsync(
account.address,
[this, state, finishOne, index,
accountId = account.address](QString payload) {
const WalletAccountRead read =
parsePublicAccount(accountId, payload);
state->publicReads[index] = read;
if (read.ok()) {
state->snapshot.accounts[index].balance =
littleEndianU128ToDecimal(read.balanceHex);
(*finishOne)();
return;
}
m_impl->logos->logos_execution_zone.get_balanceAsync(
accountId, true,
[state, finishOne, index](QString balance) {
state->snapshot.accounts[index].balance =
std::move(balance);
(*finishOne)();
});
});
}
});
});
});
};
if (currentHeight > 0) {
m_impl->logos->logos_execution_zone.sync_to_blockAsync(
currentHeight, std::move(afterSync));
} else {
afterSync(WALLET_FFI_SUCCESS);
}
});
}
bool LogosWalletProvider::save() const bool LogosWalletProvider::save() const
{ {
return m_impl->logos return m_impl->logos

View File

@ -14,9 +14,11 @@ public:
~LogosWalletProvider() override; ~LogosWalletProvider() override;
WalletSession connect(const WalletPaths& paths) override; WalletSession connect(const WalletPaths& paths) override;
void connectAsync(const WalletPaths& paths, SessionCallback callback) override;
WalletCreation createWallet(const WalletPaths& paths, WalletCreation createWallet(const WalletPaths& paths,
const QString& password) override; const QString& password) override;
WalletSnapshot snapshot(bool forceRefresh = false) override; WalletSnapshot snapshot(bool forceRefresh = false) override;
void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override;
void clearSnapshot() override; void clearSnapshot() override;
WalletAccountCreation createAccount(bool isPublic) override; WalletAccountCreation createAccount(bool isPublic) override;
WalletAccountRead readPublicAccount(const QString& accountId) const override; WalletAccountRead readPublicAccount(const QString& accountId) const override;
@ -27,6 +29,7 @@ public:
private: private:
bool sharedWalletIsOpen() const; bool sharedWalletIsOpen() const;
WalletSnapshot loadSnapshot(); WalletSnapshot loadSnapshot();
void loadSnapshotAsync(quint64 generation, SnapshotCallback callback);
bool save() const; bool save() const;
struct Impl; struct Impl;
@ -34,4 +37,5 @@ private:
WalletSnapshot m_snapshot; WalletSnapshot m_snapshot;
bool m_snapshotReady = false; bool m_snapshotReady = false;
bool m_connected = false; bool m_connected = false;
quint64 m_generation = 0;
}; };

View File

@ -5,6 +5,9 @@
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkRequest> #include <QNetworkRequest>
@ -25,6 +28,17 @@ QString toLocalPath(const QString& path)
return QUrl::fromUserInput(path).toLocalFile(); return QUrl::fromUserInput(path).toLocalFile();
return path; return path;
} }
QString configuredSequencer(const QString& path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
const QJsonDocument document = QJsonDocument::fromJson(file.readAll());
if (!document.isObject())
return {};
return document.object().value(QStringLiteral("sequencer_addr")).toString();
}
} }
WalletController::WalletController(WalletProvider& wallet, WalletController::WalletController(WalletProvider& wallet,
@ -38,7 +52,10 @@ WalletController::WalletController(WalletProvider& wallet,
m_reachabilityTimer(new QTimer(this)) m_reachabilityTimer(new QTimer(this))
{ {
m_state.walletHome = defaultWalletHome(); m_state.walletHome = defaultWalletHome();
m_state.configPath = defaultConfigPath();
m_state.storagePath = defaultStoragePath();
m_state.walletExists = QFileInfo::exists(defaultStoragePath()); m_state.walletExists = QFileInfo::exists(defaultStoragePath());
m_state.sequencerAddress = configuredSequencer(defaultConfigPath());
m_reachabilityTimer->setInterval(10000); m_reachabilityTimer->setInterval(10000);
connect(m_reachabilityTimer, &QTimer::timeout, connect(m_reachabilityTimer, &QTimer::timeout,
@ -83,20 +100,60 @@ void WalletController::openOnStartup()
const QString config = defaultConfigPath(); const QString config = defaultConfigPath();
const QString storage = defaultStoragePath(); const QString storage = defaultStoragePath();
const WalletSession session = m_wallet.connect({ config, storage }); beginOpen(config, storage);
if (session.failure == WalletFailure::WalletMissing) }
return;
if (!session.ok()) { bool WalletController::beginOpen(const QString& config, const QString& storage)
qWarning() << "WalletController: wallet connection failed" {
<< walletFailureCode(session.failure); if (m_state.syncStatus == QStringLiteral("opening")
return; || m_state.syncStatus == QStringLiteral("syncing")) {
return false;
} }
const quint64 generation = ++m_operationGeneration;
m_state.configPath = config; m_state.configPath = config;
m_state.storagePath = storage; m_state.storagePath = storage;
m_state.walletExists = QFileInfo::exists(storage) || session.adopted; m_state.syncStatus = QStringLiteral("opening");
m_state.isWalletOpen = true; m_state.syncError.clear();
applySnapshot(session.snapshot); const QString endpoint = configuredSequencer(config);
if (!endpoint.isEmpty())
m_state.sequencerAddress = endpoint;
emit stateChanged();
QTimer::singleShot(0, this, [this, generation]() {
if (generation == m_operationGeneration
&& m_state.syncStatus == QStringLiteral("opening")) {
m_state.syncStatus = QStringLiteral("syncing");
emit stateChanged();
}
});
m_wallet.connectAsync({ config, storage },
[this, generation, config, storage](WalletSession session) {
if (generation != m_operationGeneration)
return;
if (session.failure == WalletFailure::WalletMissing) {
m_state.syncStatus = QStringLiteral("closed");
m_state.walletExists = false;
emit stateChanged();
return;
}
if (!session.ok()) {
qWarning() << "WalletController: wallet connection failed"
<< walletFailureCode(session.failure);
m_state.syncStatus = QStringLiteral("error");
m_state.syncError = walletFailureCode(session.failure);
emit stateChanged();
return;
}
m_state.configPath = config;
m_state.storagePath = storage;
m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
m_state.isWalletOpen = true;
m_state.syncStatus = QStringLiteral("ready");
applySnapshot(session.snapshot);
});
return true;
} }
QString WalletController::createDefaultWallet(const QString& password) QString WalletController::createDefaultWallet(const QString& password)
@ -130,6 +187,7 @@ QString WalletController::createWallet(const QString& configPath,
} }
m_state.isWalletOpen = true; m_state.isWalletOpen = true;
m_state.syncStatus = QStringLiteral("ready");
applySnapshot(creation.snapshot); applySnapshot(creation.snapshot);
return creation.mnemonic; return creation.mnemonic;
} }
@ -140,26 +198,17 @@ bool WalletController::open()
? defaultConfigPath() : m_state.configPath; ? defaultConfigPath() : m_state.configPath;
const QString storage = m_state.storagePath.isEmpty() const QString storage = m_state.storagePath.isEmpty()
? defaultStoragePath() : m_state.storagePath; ? defaultStoragePath() : m_state.storagePath;
const WalletSession session = m_wallet.connect({ config, storage });
if (!session.ok()) {
qWarning() << "WalletController: wallet open failed"
<< walletFailureCode(session.failure);
return false;
}
m_state.configPath = config;
m_state.storagePath = storage;
m_state.walletExists = true;
m_state.isWalletOpen = true;
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false); QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
applySnapshot(session.snapshot); return beginOpen(config, storage);
return true;
} }
void WalletController::disconnect() void WalletController::disconnect()
{ {
++m_operationGeneration;
m_wallet.disconnect(); m_wallet.disconnect();
m_state.isWalletOpen = false; m_state.isWalletOpen = false;
m_state.syncStatus = QStringLiteral("closed");
m_state.syncError.clear();
m_accountModel->replaceAccounts({}); m_accountModel->replaceAccounts({});
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, true); QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, true);
emit stateChanged(); emit stateChanged();
@ -184,13 +233,26 @@ QString WalletController::createAccount(bool isPublic)
void WalletController::refresh() void WalletController::refresh()
{ {
const WalletSnapshot next = m_wallet.snapshot(true); if (!m_state.isWalletOpen || m_state.syncStatus == QStringLiteral("syncing"))
if (next.ok()) { return;
applySnapshot(next); const quint64 generation = ++m_operationGeneration;
} else { m_state.syncStatus = QStringLiteral("syncing");
qWarning() << "WalletController: wallet refresh failed" m_state.syncError.clear();
<< walletFailureCode(next.failure); emit stateChanged();
} m_wallet.snapshotAsync(true, [this, generation](WalletSnapshot next) {
if (generation != m_operationGeneration)
return;
if (next.ok()) {
m_state.syncStatus = QStringLiteral("ready");
applySnapshot(next);
} else {
qWarning() << "WalletController: wallet refresh failed"
<< walletFailureCode(next.failure);
m_state.syncStatus = QStringLiteral("error");
m_state.syncError = walletFailureCode(next.failure);
emit stateChanged();
}
});
} }
QString WalletController::balance(const QString& accountId, bool isPublic) QString WalletController::balance(const QString& accountId, bool isPublic)
@ -208,7 +270,8 @@ void WalletController::applySnapshot(const WalletSnapshot& snapshot)
m_accountModel->replaceAccounts(snapshot.accounts); m_accountModel->replaceAccounts(snapshot.accounts);
m_state.lastSyncedBlock = static_cast<int>(snapshot.lastSyncedBlock); m_state.lastSyncedBlock = static_cast<int>(snapshot.lastSyncedBlock);
m_state.currentBlockHeight = static_cast<int>(snapshot.currentBlockHeight); m_state.currentBlockHeight = static_cast<int>(snapshot.currentBlockHeight);
m_state.sequencerAddress = snapshot.sequencerAddress; if (!snapshot.sequencerAddress.isEmpty())
m_state.sequencerAddress = snapshot.sequencerAddress;
emit stateChanged(); emit stateChanged();
checkReachability(); checkReachability();
} }

View File

@ -19,6 +19,13 @@ struct WalletUiState {
int currentBlockHeight = 0; int currentBlockHeight = 0;
QString sequencerAddress; QString sequencerAddress;
bool sequencerReachable = true; bool sequencerReachable = true;
QString syncStatus = QStringLiteral("closed");
QString syncError;
bool canSubmit() const
{
return isWalletOpen && syncStatus == QStringLiteral("ready");
}
}; };
class WalletController final : public QObject { class WalletController final : public QObject {
@ -54,6 +61,7 @@ private:
QString defaultStoragePath() const; QString defaultStoragePath() const;
void openOnStartup(); void openOnStartup();
bool beginOpen(const QString& config, const QString& storage);
void applySnapshot(const WalletSnapshot& snapshot); void applySnapshot(const WalletSnapshot& snapshot);
void checkReachability(); void checkReachability();
@ -64,4 +72,5 @@ private:
QNetworkAccessManager* m_network; QNetworkAccessManager* m_network;
QTimer* m_reachabilityTimer; QTimer* m_reachabilityTimer;
bool m_started = false; bool m_started = false;
quint64 m_operationGeneration = 0;
}; };

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <functional>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QVector> #include <QVector>
@ -92,12 +93,17 @@ struct WalletSubmission {
class WalletProvider { class WalletProvider {
public: public:
using SessionCallback = std::function<void(WalletSession)>;
using SnapshotCallback = std::function<void(WalletSnapshot)>;
virtual ~WalletProvider() = default; virtual ~WalletProvider() = default;
virtual WalletSession connect(const WalletPaths& paths) = 0; virtual WalletSession connect(const WalletPaths& paths) = 0;
virtual void connectAsync(const WalletPaths& paths, SessionCallback callback) = 0;
virtual WalletCreation createWallet(const WalletPaths& paths, virtual WalletCreation createWallet(const WalletPaths& paths,
const QString& password) = 0; const QString& password) = 0;
virtual WalletSnapshot snapshot(bool forceRefresh = false) = 0; virtual WalletSnapshot snapshot(bool forceRefresh = false) = 0;
virtual void snapshotAsync(bool forceRefresh, SnapshotCallback callback) = 0;
virtual void clearSnapshot() = 0; virtual void clearSnapshot() = 0;
virtual WalletAccountCreation createAccount(bool isPublic) = 0; virtual WalletAccountCreation createAccount(bool isPublic) = 0;
virtual WalletAccountRead readPublicAccount(const QString& accountId) const = 0; virtual WalletAccountRead readPublicAccount(const QString& accountId) const = 0;

View File

@ -39,6 +39,7 @@ QVariantMap accountEntry(const QString& id, bool isPublic)
{ QStringLiteral("is_public"), isPublic }, { QStringLiteral("is_public"), isPublic },
}; };
} }
} }
class LogosWalletProviderTest : public QObject { class LogosWalletProviderTest : public QObject {
@ -52,12 +53,13 @@ private slots:
void fallsBackToBalanceWhenPublicReadFails(); void fallsBackToBalanceWhenPublicReadFails();
void createsAndPersistsAccounts(); void createsAndPersistsAccounts();
void preservesCreatedAccountWhenPublicReadFails(); void preservesCreatedAccountWhenPublicReadFails();
void preservesCreatedAccountWhenSnapshotRefreshFails(); void createdAccountDoesNotRescanWallet();
void dispatchesExactGenericTransaction(); void dispatchesExactGenericTransaction();
void rejectsInvalidSubmissionResponses(); void rejectsInvalidSubmissionResponses();
void exposesStableAccountModelRoles(); void exposesStableAccountModelRoles();
void fakeProviderImplementsConsumerContract(); void fakeProviderImplementsConsumerContract();
void controllerOwnsUiWalletFlow(); void controllerOwnsUiWalletFlow();
void controllerOpenDoesNotWaitForWalletSync();
void controllerStopsReachabilityChecksAfterDisconnect(); void controllerStopsReachabilityChecksAfterDisconnect();
}; };
@ -257,7 +259,7 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenPublicReadFails()
QCOMPARE(creation.snapshot.accounts.at(0).balance, QStringLiteral("7")); QCOMPARE(creation.snapshot.accounts.at(0).balance, QStringLiteral("7"));
} }
void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails() void LogosWalletProviderTest::createdAccountDoesNotRescanWallet()
{ {
LogosModules modules; LogosModules modules;
modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer"); modules.logos_execution_zone.sequencerAddress = QStringLiteral("http://sequencer");
@ -268,11 +270,13 @@ void LogosWalletProviderTest::preservesCreatedAccountWhenSnapshotRefreshFails()
modules.logos_execution_zone.currentBlockHeight = 1; modules.logos_execution_zone.currentBlockHeight = 1;
modules.logos_execution_zone.syncResult = 1; modules.logos_execution_zone.syncResult = 1;
const int syncCalls = modules.logos_execution_zone.syncCalls;
const WalletAccountCreation creation = provider.createAccount(true); const WalletAccountCreation creation = provider.createAccount(true);
QVERIFY(creation.ok()); QVERIFY(creation.ok());
QCOMPARE(creation.accountId, ACCOUNT_A); QCOMPARE(creation.accountId, ACCOUNT_A);
QCOMPARE(creation.snapshot.failure, WalletFailure::ReadFailed); QVERIFY(creation.snapshot.ok());
QCOMPARE(modules.logos_execution_zone.syncCalls, syncCalls);
} }
void LogosWalletProviderTest::dispatchesExactGenericTransaction() void LogosWalletProviderTest::dispatchesExactGenericTransaction()
@ -419,6 +423,33 @@ void LogosWalletProviderTest::controllerOwnsUiWalletFlow()
settings.clear(); settings.clear();
} }
void LogosWalletProviderTest::controllerOpenDoesNotWaitForWalletSync()
{
const QString settingsApplication = QStringLiteral("WalletAsyncOpenTest");
QSettings settings(QStringLiteral("Logos"), settingsApplication);
settings.clear();
FakeWalletProvider provider;
provider.deferAsync = true;
provider.connectResult.snapshot.accounts = {
{ ACCOUNT_A, QStringLiteral("5"), true },
};
WalletController controller(provider, settingsApplication);
QVERIFY(controller.open());
QCOMPARE(provider.connectCalls, 1);
QVERIFY(!controller.state().isWalletOpen);
QCOMPARE(controller.state().syncStatus, QStringLiteral("opening"));
QCOMPARE(controller.accountModel()->count(), 0);
provider.finishConnect();
QVERIFY(controller.state().isWalletOpen);
QVERIFY(controller.state().canSubmit());
QCOMPARE(controller.state().syncStatus, QStringLiteral("ready"));
QCOMPARE(controller.accountModel()->count(), 1);
settings.clear();
}
void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect() void LogosWalletProviderTest::controllerStopsReachabilityChecksAfterDisconnect()
{ {
const QString settingsApplication = QStringLiteral("WalletReachabilityTest"); const QString settingsApplication = QStringLiteral("WalletReachabilityTest");

View File

@ -6,6 +6,8 @@
#include <QVariant> #include <QVariant>
#include <QVariantList> #include <QVariantList>
#include <functional>
class LogosAPI; class LogosAPI;
class FakeExecutionZone { class FakeExecutionZone {
@ -48,6 +50,13 @@ public:
return openResult; return openResult;
} }
void openAsync(const QString& config,
const QString& storage,
std::function<void(int)> callback)
{
callback(open(config, storage));
}
QString create_new(const QString& config, QString create_new(const QString& config,
const QString& storage, const QString& storage,
const QString& password) const QString& password)
@ -64,36 +73,69 @@ public:
return saveResult; return saveResult;
} }
void saveAsync(std::function<void(int)> callback) { callback(save()); }
QString create_account_public() { return publicAccountId; } QString create_account_public() { return publicAccountId; }
QString create_account_private() { return privateAccountId; } QString create_account_private() { return privateAccountId; }
int get_last_synced_block() const { return lastSyncedBlock; } int get_last_synced_block() const { return lastSyncedBlock; }
int get_current_block_height() const { return currentBlockHeight; } int get_current_block_height() const { return currentBlockHeight; }
void get_last_synced_blockAsync(std::function<void(int)> callback)
{
callback(get_last_synced_block());
}
void get_current_block_heightAsync(std::function<void(int)> callback)
{
callback(get_current_block_height());
}
int sync_to_block(quint64) int sync_to_block(quint64)
{ {
++syncCalls; ++syncCalls;
return syncResult; return syncResult;
} }
void sync_to_blockAsync(int blockId, std::function<void(int)> callback)
{
callback(sync_to_block(static_cast<quint64>(blockId)));
}
QString get_sequencer_addr() const { return sequencerAddress; } QString get_sequencer_addr() const { return sequencerAddress; }
void get_sequencer_addrAsync(std::function<void(QString)> callback)
{
callback(get_sequencer_addr());
}
QVariantList list_accounts() QVariantList list_accounts()
{ {
++listCalls; ++listCalls;
return accounts; return accounts;
} }
void list_accountsAsync(std::function<void(QVariantList)> callback)
{
callback(list_accounts());
}
QString get_account_public(const QString& accountId) QString get_account_public(const QString& accountId)
{ {
++publicReadCalls; ++publicReadCalls;
return publicAccounts.value(accountId); return publicAccounts.value(accountId);
} }
void get_account_publicAsync(const QString& accountId,
std::function<void(QString)> callback)
{
callback(get_account_public(accountId));
}
QString get_balance(const QString& accountId, bool) const QString get_balance(const QString& accountId, bool) const
{ {
return balances.value(accountId); return balances.value(accountId);
} }
void get_balanceAsync(const QString& accountId,
bool isPublic,
std::function<void(QString)> callback)
{
callback(get_balance(accountId, isPublic));
}
QString send_generic_public_transaction( QString send_generic_public_transaction(
const QStringList& accountIds, const QStringList& accountIds,

View File

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <utility>
#include "WalletProvider.h" #include "WalletProvider.h"
class FakeWalletProvider final : public WalletProvider { class FakeWalletProvider final : public WalletProvider {
@ -23,6 +25,9 @@ public:
bool lastAccountWasPublic = false; bool lastAccountWasPublic = false;
WalletPaths lastPaths; WalletPaths lastPaths;
WalletTransaction lastTransaction; WalletTransaction lastTransaction;
bool deferAsync = false;
SessionCallback pendingConnectCallback;
SnapshotCallback pendingSnapshotCallback;
WalletSession connect(const WalletPaths& paths) override WalletSession connect(const WalletPaths& paths) override
{ {
@ -31,6 +36,16 @@ public:
return connectResult; return connectResult;
} }
void connectAsync(const WalletPaths& paths, SessionCallback callback) override
{
++connectCalls;
lastPaths = paths;
if (deferAsync)
pendingConnectCallback = std::move(callback);
else
callback(connectResult);
}
WalletCreation createWallet(const WalletPaths& paths, WalletCreation createWallet(const WalletPaths& paths,
const QString&) override const QString&) override
{ {
@ -46,6 +61,16 @@ public:
return snapshotResult; return snapshotResult;
} }
void snapshotAsync(bool forceRefresh, SnapshotCallback callback) override
{
++snapshotCalls;
lastForceRefresh = forceRefresh;
if (deferAsync)
pendingSnapshotCallback = std::move(callback);
else
callback(snapshotResult);
}
void clearSnapshot() override { ++clearCalls; } void clearSnapshot() override { ++clearCalls; }
WalletAccountCreation createAccount(bool isPublic) override WalletAccountCreation createAccount(bool isPublic) override
@ -72,4 +97,18 @@ public:
} }
void disconnect() override { ++disconnectCalls; } void disconnect() override { ++disconnectCalls; }
void finishConnect()
{
SessionCallback callback = std::move(pendingConnectCallback);
if (callback)
callback(connectResult);
}
void finishSnapshot()
{
SnapshotCallback callback = std::move(pendingSnapshotCallback);
if (callback)
callback(snapshotResult);
}
}; };