mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
feat(wallet): add reusable wallet modules
Add program-neutral wallet access, a stable account model, reusable QML controls, transaction confirmation, submitted-transaction presentation, and isolated contract tests.
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
#include "LogosWalletProvider.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "logos_sdk.h"
|
||||
|
||||
namespace {
|
||||
constexpr int WALLET_FFI_SUCCESS = 0;
|
||||
|
||||
bool isHex(const QString& value, qsizetype size, bool lowercaseOnly = true)
|
||||
{
|
||||
if (value.size() != size)
|
||||
return false;
|
||||
|
||||
for (const QChar character : value) {
|
||||
const bool digit = character >= QLatin1Char('0')
|
||||
&& character <= QLatin1Char('9');
|
||||
const bool lowercase = character >= QLatin1Char('a')
|
||||
&& character <= QLatin1Char('f');
|
||||
const bool uppercase = character >= QLatin1Char('A')
|
||||
&& character <= QLatin1Char('F');
|
||||
if (!digit && !lowercase && (!uppercase || lowercaseOnly))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString littleEndianU128ToDecimal(const QString& value)
|
||||
{
|
||||
if (!isHex(value, 32))
|
||||
return {};
|
||||
|
||||
QString decimal = QStringLiteral("0");
|
||||
for (int byteIndex = 15; byteIndex >= 0; --byteIndex) {
|
||||
bool parsed = false;
|
||||
int carry = value.mid(byteIndex * 2, 2).toInt(&parsed, 16);
|
||||
if (!parsed)
|
||||
return {};
|
||||
|
||||
for (qsizetype digit = decimal.size(); digit-- > 0;) {
|
||||
const int next = (decimal.at(digit).unicode() - QLatin1Char('0').unicode())
|
||||
* 256 + carry;
|
||||
decimal[digit] = QLatin1Char(static_cast<char>('0' + next % 10));
|
||||
carry = next / 10;
|
||||
}
|
||||
while (carry > 0) {
|
||||
decimal.prepend(QLatin1Char(static_cast<char>('0' + carry % 10)));
|
||||
carry /= 10;
|
||||
}
|
||||
}
|
||||
|
||||
return decimal;
|
||||
}
|
||||
|
||||
WalletSession failedSession(WalletFailure failure)
|
||||
{
|
||||
WalletSession session;
|
||||
session.failure = failure;
|
||||
session.snapshot.failure = failure;
|
||||
return session;
|
||||
}
|
||||
|
||||
WalletCreation failedCreation(WalletFailure failure)
|
||||
{
|
||||
WalletCreation creation;
|
||||
creation.failure = failure;
|
||||
creation.snapshot.failure = failure;
|
||||
return creation;
|
||||
}
|
||||
}
|
||||
|
||||
struct LogosWalletProvider::Impl {
|
||||
explicit Impl(LogosAPI* api)
|
||||
: ownedLogos(std::make_unique<LogosModules>(api)), logos(ownedLogos.get())
|
||||
{
|
||||
}
|
||||
|
||||
explicit Impl(LogosModules* value)
|
||||
: logos(value)
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<LogosModules> ownedLogos;
|
||||
LogosModules* logos = nullptr;
|
||||
};
|
||||
|
||||
LogosWalletProvider::LogosWalletProvider(LogosAPI* api)
|
||||
: m_impl(std::make_unique<Impl>(api))
|
||||
{
|
||||
}
|
||||
|
||||
LogosWalletProvider::LogosWalletProvider(LogosModules* logos)
|
||||
: m_impl(std::make_unique<Impl>(logos))
|
||||
{
|
||||
}
|
||||
|
||||
LogosWalletProvider::~LogosWalletProvider()
|
||||
{
|
||||
if (m_connected)
|
||||
save();
|
||||
}
|
||||
|
||||
WalletSession LogosWalletProvider::connect(const WalletPaths& paths)
|
||||
{
|
||||
clearSnapshot();
|
||||
if (!m_impl->logos)
|
||||
return failedSession(WalletFailure::WalletUnavailable);
|
||||
|
||||
WalletSession session;
|
||||
if (sharedWalletIsOpen()) {
|
||||
session.adopted = true;
|
||||
} else {
|
||||
if (!QFileInfo::exists(paths.storage))
|
||||
return failedSession(WalletFailure::WalletMissing);
|
||||
if (m_impl->logos->logos_execution_zone.open(paths.config, paths.storage)
|
||||
!= WALLET_FFI_SUCCESS) {
|
||||
return failedSession(WalletFailure::OpenFailed);
|
||||
}
|
||||
}
|
||||
|
||||
m_connected = true;
|
||||
session.snapshot = snapshot(true);
|
||||
session.failure = session.snapshot.failure;
|
||||
return session;
|
||||
}
|
||||
|
||||
WalletCreation LogosWalletProvider::createWallet(const WalletPaths& paths,
|
||||
const QString& password)
|
||||
{
|
||||
clearSnapshot();
|
||||
if (!m_impl->logos)
|
||||
return failedCreation(WalletFailure::WalletUnavailable);
|
||||
|
||||
const QFileInfo configInfo(paths.config);
|
||||
const QFileInfo storageInfo(paths.storage);
|
||||
if (!QDir().mkpath(configInfo.absolutePath())
|
||||
|| !QDir().mkpath(storageInfo.absolutePath())) {
|
||||
return failedCreation(WalletFailure::CreateFailed);
|
||||
}
|
||||
|
||||
WalletCreation creation;
|
||||
creation.mnemonic = m_impl->logos->logos_execution_zone.create_new(
|
||||
paths.config, paths.storage, password);
|
||||
if (creation.mnemonic.isEmpty())
|
||||
return failedCreation(WalletFailure::CreateFailed);
|
||||
|
||||
m_connected = true;
|
||||
if (!save()) {
|
||||
creation.failure = WalletFailure::SaveFailed;
|
||||
creation.snapshot.failure = creation.failure;
|
||||
return creation;
|
||||
}
|
||||
|
||||
creation.snapshot = snapshot(true);
|
||||
creation.failure = creation.snapshot.failure;
|
||||
return creation;
|
||||
}
|
||||
|
||||
WalletSnapshot LogosWalletProvider::snapshot(bool forceRefresh)
|
||||
{
|
||||
if (m_snapshotReady && !forceRefresh)
|
||||
return m_snapshot;
|
||||
if (!m_connected) {
|
||||
WalletSnapshot result;
|
||||
result.failure = WalletFailure::WalletUnavailable;
|
||||
return result;
|
||||
}
|
||||
|
||||
WalletSnapshot result = loadSnapshot();
|
||||
if (result.ok()) {
|
||||
m_snapshot = result;
|
||||
m_snapshotReady = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void LogosWalletProvider::clearSnapshot()
|
||||
{
|
||||
m_snapshot = {};
|
||||
m_snapshotReady = false;
|
||||
}
|
||||
|
||||
WalletAccountCreation LogosWalletProvider::createAccount(bool isPublic)
|
||||
{
|
||||
WalletAccountCreation creation;
|
||||
if (!m_connected || !m_impl->logos) {
|
||||
creation.failure = WalletFailure::WalletUnavailable;
|
||||
return creation;
|
||||
}
|
||||
|
||||
creation.accountId = isPublic
|
||||
? m_impl->logos->logos_execution_zone.create_account_public()
|
||||
: m_impl->logos->logos_execution_zone.create_account_private();
|
||||
if (!isHex(creation.accountId, 64)) {
|
||||
creation.failure = WalletFailure::CreateFailed;
|
||||
return creation;
|
||||
}
|
||||
if (!save()) {
|
||||
creation.failure = WalletFailure::SaveFailed;
|
||||
return creation;
|
||||
}
|
||||
|
||||
if (isPublic)
|
||||
creation.publicAccount = readPublicAccount(creation.accountId);
|
||||
|
||||
clearSnapshot();
|
||||
creation.snapshot = snapshot(true);
|
||||
return creation;
|
||||
}
|
||||
|
||||
WalletAccountRead LogosWalletProvider::readPublicAccount(const QString& accountId) const
|
||||
{
|
||||
WalletAccountRead read;
|
||||
read.accountId = accountId;
|
||||
if (!m_impl->logos || !isHex(accountId, 64))
|
||||
return read;
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(
|
||||
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(
|
||||
const WalletTransaction& transaction)
|
||||
{
|
||||
WalletSubmission submission;
|
||||
if (!m_connected || !m_impl->logos) {
|
||||
submission.failure = WalletFailure::WalletUnavailable;
|
||||
return submission;
|
||||
}
|
||||
if (!isHex(transaction.programId, 64)
|
||||
|| transaction.accountIds.size() != transaction.signingRequirements.size()) {
|
||||
submission.failure = WalletFailure::InvalidRequest;
|
||||
return submission;
|
||||
}
|
||||
for (const QString& accountId : transaction.accountIds) {
|
||||
if (!isHex(accountId, 64)) {
|
||||
submission.failure = WalletFailure::InvalidRequest;
|
||||
return submission;
|
||||
}
|
||||
}
|
||||
|
||||
QVariantList signingRequirements;
|
||||
signingRequirements.reserve(transaction.signingRequirements.size());
|
||||
for (bool required : transaction.signingRequirements)
|
||||
signingRequirements.append(required);
|
||||
|
||||
QVariantList instruction;
|
||||
instruction.reserve(transaction.instruction.size());
|
||||
for (quint32 word : transaction.instruction)
|
||||
instruction.append(word);
|
||||
|
||||
const QString response =
|
||||
m_impl->logos->logos_execution_zone.send_generic_public_transaction(
|
||||
transaction.accountIds,
|
||||
signingRequirements,
|
||||
QVariant::fromValue(instruction),
|
||||
transaction.programId);
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(response.toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
submission.failure = WalletFailure::SubmissionFailed;
|
||||
return submission;
|
||||
}
|
||||
|
||||
const QJsonObject result = document.object();
|
||||
const QJsonValue success = result.value(QStringLiteral("success"));
|
||||
const QJsonValue error = result.value(QStringLiteral("error"));
|
||||
const QString hash = result.value(QStringLiteral("tx_hash")).toString();
|
||||
const bool emptyError = error.isUndefined()
|
||||
|| error.isNull()
|
||||
|| (error.isString() && error.toString().isEmpty());
|
||||
if (!success.isBool()
|
||||
|| !success.toBool()
|
||||
|| !emptyError
|
||||
|| !isHex(hash, 64, false)) {
|
||||
submission.failure = WalletFailure::SubmissionFailed;
|
||||
return submission;
|
||||
}
|
||||
|
||||
submission.nativeHash = hash.toLower();
|
||||
return submission;
|
||||
}
|
||||
|
||||
void LogosWalletProvider::disconnect()
|
||||
{
|
||||
if (m_connected)
|
||||
save();
|
||||
clearSnapshot();
|
||||
m_connected = false;
|
||||
}
|
||||
|
||||
bool LogosWalletProvider::sharedWalletIsOpen() const
|
||||
{
|
||||
if (!m_impl->logos)
|
||||
return false;
|
||||
if (!m_impl->logos->logos_execution_zone.get_sequencer_addr().isEmpty())
|
||||
return true;
|
||||
return !m_impl->logos->logos_execution_zone.list_accounts().isEmpty();
|
||||
}
|
||||
|
||||
WalletSnapshot LogosWalletProvider::loadSnapshot()
|
||||
{
|
||||
WalletSnapshot result;
|
||||
result.currentBlockHeight = static_cast<quint64>(
|
||||
qMax(0, m_impl->logos->logos_execution_zone.get_current_block_height()));
|
||||
if (result.currentBlockHeight > 0
|
||||
&& m_impl->logos->logos_execution_zone.sync_to_block(result.currentBlockHeight)
|
||||
!= WALLET_FFI_SUCCESS) {
|
||||
result.failure = WalletFailure::ReadFailed;
|
||||
return result;
|
||||
}
|
||||
result.lastSyncedBlock = static_cast<quint64>(
|
||||
qMax(0, m_impl->logos->logos_execution_zone.get_last_synced_block()));
|
||||
result.sequencerAddress = m_impl->logos->logos_execution_zone.get_sequencer_addr();
|
||||
|
||||
const QVariantList entries = m_impl->logos->logos_execution_zone.list_accounts();
|
||||
result.accounts.reserve(entries.size());
|
||||
result.publicAccountReads.reserve(entries.size());
|
||||
for (const QVariant& value : entries) {
|
||||
const QVariantMap entry = value.toMap();
|
||||
const QString address = entry.value(QStringLiteral("account_id")).toString();
|
||||
if (entry.isEmpty() || !isHex(address, 64)) {
|
||||
result.failure = WalletFailure::ReadFailed;
|
||||
return result;
|
||||
}
|
||||
|
||||
WalletAccount account;
|
||||
account.address = address;
|
||||
account.isPublic = entry.value(QStringLiteral("is_public"), true).toBool();
|
||||
if (account.isPublic) {
|
||||
const WalletAccountRead read = readPublicAccount(address);
|
||||
result.publicAccountReads.append(read);
|
||||
account.balance = read.ok()
|
||||
? littleEndianU128ToDecimal(read.balanceHex)
|
||||
: m_impl->logos->logos_execution_zone.get_balance(address, true);
|
||||
} else {
|
||||
account.balance = m_impl->logos->logos_execution_zone.get_balance(address, false);
|
||||
}
|
||||
result.accounts.append(account);
|
||||
}
|
||||
|
||||
if (!save())
|
||||
result.failure = WalletFailure::SaveFailed;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool LogosWalletProvider::save() const
|
||||
{
|
||||
return m_impl->logos
|
||||
&& m_impl->logos->logos_execution_zone.save() == WALLET_FFI_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class LogosAPI;
|
||||
struct LogosModules;
|
||||
|
||||
class LogosWalletProvider final : public WalletProvider {
|
||||
public:
|
||||
explicit LogosWalletProvider(LogosAPI* api);
|
||||
explicit LogosWalletProvider(LogosModules* logos);
|
||||
~LogosWalletProvider() override;
|
||||
|
||||
WalletSession connect(const WalletPaths& paths) override;
|
||||
WalletCreation createWallet(const WalletPaths& paths,
|
||||
const QString& password) override;
|
||||
WalletSnapshot snapshot(bool forceRefresh = false) override;
|
||||
void clearSnapshot() override;
|
||||
WalletAccountCreation createAccount(bool isPublic) override;
|
||||
WalletAccountRead readPublicAccount(const QString& accountId) const override;
|
||||
WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) override;
|
||||
void disconnect() override;
|
||||
|
||||
private:
|
||||
bool sharedWalletIsOpen() const;
|
||||
WalletSnapshot loadSnapshot();
|
||||
bool save() const;
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
WalletSnapshot m_snapshot;
|
||||
bool m_snapshotReady = false;
|
||||
bool m_connected = false;
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "WalletAccountModel.h"
|
||||
|
||||
WalletAccountModel::WalletAccountModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int WalletAccountModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : m_accounts.size();
|
||||
}
|
||||
|
||||
QVariant WalletAccountModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_accounts.size())
|
||||
return {};
|
||||
|
||||
const Entry& account = m_accounts.at(index.row());
|
||||
switch (role) {
|
||||
case NameRole:
|
||||
return account.name;
|
||||
case AddressRole:
|
||||
return account.address;
|
||||
case BalanceRole:
|
||||
return account.balance;
|
||||
case IsPublicRole:
|
||||
return account.isPublic;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> WalletAccountModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{ NameRole, "name" },
|
||||
{ AddressRole, "address" },
|
||||
{ BalanceRole, "balance" },
|
||||
{ IsPublicRole, "isPublic" },
|
||||
};
|
||||
}
|
||||
|
||||
void WalletAccountModel::replaceAccounts(const QVector<WalletAccount>& accounts)
|
||||
{
|
||||
beginResetModel();
|
||||
const qsizetype oldCount = m_accounts.size();
|
||||
m_accounts.clear();
|
||||
m_accounts.reserve(accounts.size());
|
||||
for (qsizetype index = 0; index < accounts.size(); ++index) {
|
||||
const WalletAccount& account = accounts.at(index);
|
||||
m_accounts.append({
|
||||
QStringLiteral("Account %1").arg(index + 1),
|
||||
account.address,
|
||||
account.balance,
|
||||
account.isPublic,
|
||||
});
|
||||
}
|
||||
endResetModel();
|
||||
if (oldCount != m_accounts.size())
|
||||
emit countChanged();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QVector>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class WalletAccountModel final : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
|
||||
public:
|
||||
enum Role {
|
||||
NameRole = Qt::UserRole + 1,
|
||||
AddressRole,
|
||||
BalanceRole,
|
||||
IsPublicRole,
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
|
||||
explicit WalletAccountModel(QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void replaceAccounts(const QVector<WalletAccount>& accounts);
|
||||
int count() const { return m_accounts.size(); }
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
QString name;
|
||||
QString address;
|
||||
QString balance;
|
||||
bool isPublic = true;
|
||||
};
|
||||
|
||||
QVector<Entry> m_accounts;
|
||||
};
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "WalletController.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "WalletAccountModel.h"
|
||||
|
||||
namespace {
|
||||
const char SETTINGS_ORG[] = "Logos";
|
||||
const char DISCONNECTED_KEY[] = "disconnected";
|
||||
const char WALLET_HOME_ENV[] = "LEE_WALLET_HOME_DIR";
|
||||
|
||||
QString toLocalPath(const QString& path)
|
||||
{
|
||||
if (path.startsWith(QStringLiteral("file://")) || path.contains(QLatin1Char('/')))
|
||||
return QUrl::fromUserInput(path).toLocalFile();
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
WalletController::WalletController(WalletProvider& wallet,
|
||||
QString settingsApplication,
|
||||
QObject* parent)
|
||||
: QObject(parent),
|
||||
m_wallet(wallet),
|
||||
m_settingsApplication(std::move(settingsApplication)),
|
||||
m_accountModel(new WalletAccountModel(this)),
|
||||
m_network(new QNetworkAccessManager(this)),
|
||||
m_reachabilityTimer(new QTimer(this))
|
||||
{
|
||||
m_state.walletHome = defaultWalletHome();
|
||||
m_state.walletExists = QFileInfo::exists(defaultStoragePath());
|
||||
|
||||
m_reachabilityTimer->setInterval(10000);
|
||||
connect(m_reachabilityTimer, &QTimer::timeout,
|
||||
this, &WalletController::checkReachability);
|
||||
}
|
||||
|
||||
WalletController::~WalletController() = default;
|
||||
|
||||
QString WalletController::defaultWalletHome()
|
||||
{
|
||||
const QByteArray override = qgetenv(WALLET_HOME_ENV);
|
||||
if (!override.isEmpty())
|
||||
return QString::fromLocal8Bit(override);
|
||||
return QDir::homePath() + QStringLiteral("/.lee/wallet");
|
||||
}
|
||||
|
||||
QString WalletController::defaultConfigPath() const
|
||||
{
|
||||
return m_state.walletHome + QStringLiteral("/wallet_config.json");
|
||||
}
|
||||
|
||||
QString WalletController::defaultStoragePath() const
|
||||
{
|
||||
return m_state.walletHome + QStringLiteral("/storage.json");
|
||||
}
|
||||
|
||||
void WalletController::start()
|
||||
{
|
||||
if (m_started)
|
||||
return;
|
||||
m_started = true;
|
||||
m_reachabilityTimer->start();
|
||||
QTimer::singleShot(0, this, &WalletController::openOnStartup);
|
||||
}
|
||||
|
||||
void WalletController::openOnStartup()
|
||||
{
|
||||
if (QSettings(SETTINGS_ORG, m_settingsApplication)
|
||||
.value(DISCONNECTED_KEY, false).toBool()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString config = defaultConfigPath();
|
||||
const QString storage = defaultStoragePath();
|
||||
const WalletSession session = m_wallet.connect({ config, storage });
|
||||
if (session.failure == WalletFailure::WalletMissing)
|
||||
return;
|
||||
if (!session.ok()) {
|
||||
qWarning() << "WalletController: wallet connection failed"
|
||||
<< walletFailureCode(session.failure);
|
||||
return;
|
||||
}
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = QFileInfo::exists(storage) || session.adopted;
|
||||
m_state.isWalletOpen = true;
|
||||
applySnapshot(session.snapshot);
|
||||
}
|
||||
|
||||
QString WalletController::createDefaultWallet(const QString& password)
|
||||
{
|
||||
return createWallet(defaultConfigPath(), defaultStoragePath(), password);
|
||||
}
|
||||
|
||||
QString WalletController::createWallet(const QString& configPath,
|
||||
const QString& storagePath,
|
||||
const QString& password)
|
||||
{
|
||||
const QString config = toLocalPath(configPath);
|
||||
const QString storage = toLocalPath(storagePath);
|
||||
const WalletCreation creation = m_wallet.createWallet(
|
||||
{ config, storage }, password);
|
||||
if (creation.mnemonic.isEmpty()) {
|
||||
qWarning() << "WalletController: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
|
||||
m_state.configPath = config;
|
||||
m_state.storagePath = storage;
|
||||
m_state.walletExists = true;
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, false);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "WalletController: wallet creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
emit stateChanged();
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
m_state.isWalletOpen = true;
|
||||
applySnapshot(creation.snapshot);
|
||||
return creation.mnemonic;
|
||||
}
|
||||
|
||||
bool WalletController::open()
|
||||
{
|
||||
const QString config = m_state.configPath.isEmpty()
|
||||
? defaultConfigPath() : m_state.configPath;
|
||||
const QString storage = m_state.storagePath.isEmpty()
|
||||
? 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);
|
||||
applySnapshot(session.snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WalletController::disconnect()
|
||||
{
|
||||
m_wallet.disconnect();
|
||||
m_state.isWalletOpen = false;
|
||||
m_accountModel->replaceAccounts({});
|
||||
QSettings(SETTINGS_ORG, m_settingsApplication).setValue(DISCONNECTED_KEY, true);
|
||||
emit stateChanged();
|
||||
}
|
||||
|
||||
QString WalletController::createAccount(bool isPublic)
|
||||
{
|
||||
const WalletAccountCreation creation = m_wallet.createAccount(isPublic);
|
||||
if (!creation.ok()) {
|
||||
qWarning() << "WalletController: account creation failed"
|
||||
<< walletFailureCode(creation.failure);
|
||||
return {};
|
||||
}
|
||||
if (creation.snapshot.ok()) {
|
||||
applySnapshot(creation.snapshot);
|
||||
} else {
|
||||
qWarning() << "WalletController: account refresh failed"
|
||||
<< walletFailureCode(creation.snapshot.failure);
|
||||
}
|
||||
return creation.accountId;
|
||||
}
|
||||
|
||||
void WalletController::refresh()
|
||||
{
|
||||
const WalletSnapshot next = m_wallet.snapshot(true);
|
||||
if (next.ok()) {
|
||||
applySnapshot(next);
|
||||
} else {
|
||||
qWarning() << "WalletController: wallet refresh failed"
|
||||
<< walletFailureCode(next.failure);
|
||||
}
|
||||
}
|
||||
|
||||
QString WalletController::balance(const QString& accountId, bool isPublic)
|
||||
{
|
||||
const WalletSnapshot current = m_wallet.snapshot();
|
||||
for (const WalletAccount& account : current.accounts) {
|
||||
if (account.address == accountId && account.isPublic == isPublic)
|
||||
return account.balance;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void WalletController::applySnapshot(const WalletSnapshot& snapshot)
|
||||
{
|
||||
m_accountModel->replaceAccounts(snapshot.accounts);
|
||||
m_state.lastSyncedBlock = static_cast<int>(snapshot.lastSyncedBlock);
|
||||
m_state.currentBlockHeight = static_cast<int>(snapshot.currentBlockHeight);
|
||||
m_state.sequencerAddress = snapshot.sequencerAddress;
|
||||
emit stateChanged();
|
||||
checkReachability();
|
||||
}
|
||||
|
||||
void WalletController::checkReachability()
|
||||
{
|
||||
if (!m_state.isWalletOpen || m_state.sequencerAddress.isEmpty())
|
||||
return;
|
||||
|
||||
QNetworkRequest request{QUrl(m_state.sequencerAddress)};
|
||||
request.setTransferTimeout(4000);
|
||||
QNetworkReply* reply = m_network->get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
if (!m_state.isWalletOpen) {
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
const bool receivedHttp =
|
||||
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid();
|
||||
const bool reachable = receivedHttp || reply->error() == QNetworkReply::NoError;
|
||||
if (m_state.sequencerReachable != reachable) {
|
||||
m_state.sequencerReachable = reachable;
|
||||
emit stateChanged();
|
||||
}
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "WalletProvider.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QTimer;
|
||||
class WalletAccountModel;
|
||||
|
||||
struct WalletUiState {
|
||||
bool isWalletOpen = false;
|
||||
bool walletExists = false;
|
||||
QString configPath;
|
||||
QString storagePath;
|
||||
QString walletHome;
|
||||
int lastSyncedBlock = 0;
|
||||
int currentBlockHeight = 0;
|
||||
QString sequencerAddress;
|
||||
bool sequencerReachable = true;
|
||||
};
|
||||
|
||||
class WalletController final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// The provider must outlive the controller.
|
||||
explicit WalletController(WalletProvider& wallet,
|
||||
QString settingsApplication,
|
||||
QObject* parent = nullptr);
|
||||
~WalletController() override;
|
||||
|
||||
WalletAccountModel* accountModel() const { return m_accountModel; }
|
||||
const WalletUiState& state() const { return m_state; }
|
||||
|
||||
void start();
|
||||
QString createAccount(bool isPublic);
|
||||
void refresh();
|
||||
QString balance(const QString& accountId, bool isPublic);
|
||||
QString createDefaultWallet(const QString& password);
|
||||
QString createWallet(const QString& configPath,
|
||||
const QString& storagePath,
|
||||
const QString& password);
|
||||
bool open();
|
||||
void disconnect();
|
||||
|
||||
signals:
|
||||
void stateChanged();
|
||||
|
||||
private:
|
||||
static QString defaultWalletHome();
|
||||
QString defaultConfigPath() const;
|
||||
QString defaultStoragePath() const;
|
||||
|
||||
void openOnStartup();
|
||||
void applySnapshot(const WalletSnapshot& snapshot);
|
||||
void checkReachability();
|
||||
|
||||
WalletProvider& m_wallet;
|
||||
QString m_settingsApplication;
|
||||
WalletUiState m_state;
|
||||
WalletAccountModel* m_accountModel;
|
||||
QNetworkAccessManager* m_network;
|
||||
QTimer* m_reachabilityTimer;
|
||||
bool m_started = false;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "WalletProvider.h"
|
||||
|
||||
QString walletFailureCode(WalletFailure failure)
|
||||
{
|
||||
switch (failure) {
|
||||
case WalletFailure::None:
|
||||
return {};
|
||||
case WalletFailure::WalletMissing:
|
||||
return QStringLiteral("wallet_missing");
|
||||
case WalletFailure::WalletUnavailable:
|
||||
return QStringLiteral("wallet_unavailable");
|
||||
case WalletFailure::OpenFailed:
|
||||
return QStringLiteral("open_failed");
|
||||
case WalletFailure::CreateFailed:
|
||||
return QStringLiteral("create_failed");
|
||||
case WalletFailure::SaveFailed:
|
||||
return QStringLiteral("save_failed");
|
||||
case WalletFailure::ReadFailed:
|
||||
return QStringLiteral("read_failed");
|
||||
case WalletFailure::InvalidRequest:
|
||||
return QStringLiteral("invalid_request");
|
||||
case WalletFailure::SubmissionFailed:
|
||||
return QStringLiteral("submission_failed");
|
||||
}
|
||||
return QStringLiteral("wallet_unavailable");
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
enum class WalletFailure {
|
||||
None,
|
||||
WalletMissing,
|
||||
WalletUnavailable,
|
||||
OpenFailed,
|
||||
CreateFailed,
|
||||
SaveFailed,
|
||||
ReadFailed,
|
||||
InvalidRequest,
|
||||
SubmissionFailed,
|
||||
};
|
||||
|
||||
QString walletFailureCode(WalletFailure failure);
|
||||
|
||||
struct WalletPaths {
|
||||
QString config;
|
||||
QString storage;
|
||||
};
|
||||
|
||||
struct WalletAccountRead {
|
||||
QString accountId;
|
||||
QString status = QStringLiteral("read_failed");
|
||||
QString programOwner;
|
||||
QString balanceHex;
|
||||
QString nonceHex;
|
||||
QString dataHex;
|
||||
|
||||
bool ok() const { return status == QStringLiteral("ok"); }
|
||||
};
|
||||
|
||||
struct WalletAccount {
|
||||
QString address;
|
||||
QString balance;
|
||||
bool isPublic = true;
|
||||
};
|
||||
|
||||
struct WalletSnapshot {
|
||||
WalletFailure failure = WalletFailure::None;
|
||||
QVector<WalletAccount> accounts;
|
||||
QVector<WalletAccountRead> publicAccountReads;
|
||||
quint64 lastSyncedBlock = 0;
|
||||
quint64 currentBlockHeight = 0;
|
||||
QString sequencerAddress;
|
||||
|
||||
bool ok() const { return failure == WalletFailure::None; }
|
||||
};
|
||||
|
||||
struct WalletSession {
|
||||
WalletFailure failure = WalletFailure::None;
|
||||
WalletSnapshot snapshot;
|
||||
bool adopted = false;
|
||||
|
||||
bool ok() const { return failure == WalletFailure::None; }
|
||||
};
|
||||
|
||||
struct WalletCreation {
|
||||
WalletFailure failure = WalletFailure::None;
|
||||
QString mnemonic;
|
||||
WalletSnapshot snapshot;
|
||||
|
||||
bool ok() const { return failure == WalletFailure::None; }
|
||||
};
|
||||
|
||||
struct WalletAccountCreation {
|
||||
WalletFailure failure = WalletFailure::None;
|
||||
QString accountId;
|
||||
WalletAccountRead publicAccount;
|
||||
WalletSnapshot snapshot;
|
||||
|
||||
bool ok() const { return failure == WalletFailure::None; }
|
||||
};
|
||||
|
||||
struct WalletTransaction {
|
||||
QString programId;
|
||||
QStringList accountIds;
|
||||
QVector<bool> signingRequirements;
|
||||
QVector<quint32> instruction;
|
||||
};
|
||||
|
||||
struct WalletSubmission {
|
||||
WalletFailure failure = WalletFailure::None;
|
||||
QString nativeHash;
|
||||
|
||||
bool accepted() const { return failure == WalletFailure::None && !nativeHash.isEmpty(); }
|
||||
};
|
||||
|
||||
class WalletProvider {
|
||||
public:
|
||||
virtual ~WalletProvider() = default;
|
||||
|
||||
virtual WalletSession connect(const WalletPaths& paths) = 0;
|
||||
virtual WalletCreation createWallet(const WalletPaths& paths,
|
||||
const QString& password) = 0;
|
||||
virtual WalletSnapshot snapshot(bool forceRefresh = false) = 0;
|
||||
virtual void clearSnapshot() = 0;
|
||||
virtual WalletAccountCreation createAccount(bool isPublic) = 0;
|
||||
virtual WalletAccountRead readPublicAccount(const QString& accountId) const = 0;
|
||||
virtual WalletSubmission submitPublicTransaction(
|
||||
const WalletTransaction& transaction) = 0;
|
||||
virtual void disconnect() = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user