mirror of
https://github.com/logos-blockchain/lez-explorer-ui.git
synced 2026-07-30 03:13:25 +00:00
chore: bump flakes, add Justfile and formatter, add display name to metadata
This commit is contained in:
parent
7ff7873ea3
commit
6cadac2dc3
26
.clang-format
Normal file
26
.clang-format
Normal file
@ -0,0 +1,26 @@
|
||||
BasedOnStyle: LLVM
|
||||
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 120
|
||||
|
||||
IndentAccessModifiers: false
|
||||
AccessModifierOffset: -4
|
||||
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
BinPackParameters: false
|
||||
BinPackArguments: false
|
||||
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
PenaltyBreakBeforeFirstCallParameter: 1
|
||||
|
||||
PointerAlignment: Left
|
||||
ReferenceAlignment: Left
|
||||
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
|
||||
BreakAfterReturnType: None
|
||||
PenaltyReturnTypeOnItsOwnLine: 1000
|
||||
|
||||
NamespaceIndentation: All
|
||||
20
Justfile
Normal file
20
Justfile
Normal file
@ -0,0 +1,20 @@
|
||||
default: build
|
||||
|
||||
# Build the ui_qml module plugin via logos-module-builder (-> result/lib/).
|
||||
build:
|
||||
nix build
|
||||
|
||||
# Preview the UI standalone (ui_qml modules run via the builder).
|
||||
run:
|
||||
nix run .
|
||||
|
||||
# Drop into the builder dev shell.
|
||||
develop:
|
||||
nix develop
|
||||
|
||||
clean:
|
||||
rm -rf build result rocksdb-*
|
||||
|
||||
# Format the C++ backend.
|
||||
prettify:
|
||||
nix shell nixpkgs#clang-tools -c clang-format -i src/*.cpp src/*.h
|
||||
24
flake.lock
generated
24
flake.lock
generated
@ -154,16 +154,16 @@
|
||||
"logos-module-builder": "logos-module-builder_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782891810,
|
||||
"narHash": "sha256-6fmYCL8oxvSKI6H7VuM78zjIECfdzTBYP7x7E5ryEeY=",
|
||||
"ref": "main",
|
||||
"rev": "d177da4356b48d93e1f42ffa51281c078f646844",
|
||||
"revCount": 58,
|
||||
"lastModified": 1782982957,
|
||||
"narHash": "sha256-L1W94vlMtKK5w6S9np2qFEY8tJ5M5g6Aryfrb3Bf4lo=",
|
||||
"ref": "erhant/bump-lez-and-fix-logging",
|
||||
"rev": "69dbf45a57f94345d46ceca3c9bc563ce5fab0ae",
|
||||
"revCount": 59,
|
||||
"type": "git",
|
||||
"url": "https://github.com/logos-blockchain/lez-indexer-module"
|
||||
},
|
||||
"original": {
|
||||
"ref": "main",
|
||||
"ref": "erhant/bump-lez-and-fix-logging",
|
||||
"type": "git",
|
||||
"url": "https://github.com/logos-blockchain/lez-indexer-module"
|
||||
}
|
||||
@ -3391,16 +3391,16 @@
|
||||
"rust-rapidsnark": "rust-rapidsnark"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782820738,
|
||||
"narHash": "sha256-OnXBx3nD/r7vCzZwh/RXmoqbtNF9rG+ZZsWXPsXoOzk=",
|
||||
"ref": "refs/tags/v0.2.0",
|
||||
"rev": "a58fbce2ff48c58b7bb5001b1a27e64b9596ee3a",
|
||||
"revCount": 2828,
|
||||
"lastModified": 1782941647,
|
||||
"narHash": "sha256-H7T0mBWY75tZjCf9zcQUAFteurZhqnZ+2srtdPeQTRQ=",
|
||||
"ref": "main",
|
||||
"rev": "d1637b65e2479e80bb617b900ec5d4024216457d",
|
||||
"revCount": 2830,
|
||||
"type": "git",
|
||||
"url": "https://github.com/logos-blockchain/logos-execution-zone"
|
||||
},
|
||||
"original": {
|
||||
"ref": "refs/tags/v0.2.0",
|
||||
"ref": "main",
|
||||
"type": "git",
|
||||
"url": "https://github.com/logos-blockchain/logos-execution-zone"
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
|
||||
|
||||
lez_indexer_module.url = "git+https://github.com/logos-blockchain/lez-indexer-module?ref=main";
|
||||
lez_indexer_module.url = "git+https://github.com/logos-blockchain/lez-indexer-module?ref=erhant/bump-lez-and-fix-logging";
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "lez_explorer_ui",
|
||||
"display_name": "LEZ Explorer",
|
||||
"version": "1.0.0",
|
||||
"type": "ui_qml",
|
||||
"interface": "universal",
|
||||
"category": "explorer",
|
||||
"description": "LEZ Explorer UI - A block explorer for the Logos Execution Zone",
|
||||
"description": "Logos Execution Zone Block Explorer",
|
||||
"main": "lez_explorer_ui_plugin",
|
||||
"icon": "src/qml/icons/box.svg",
|
||||
"view": "qml/Main.qml",
|
||||
|
||||
@ -24,312 +24,322 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kPollIntervalMs = 2000;
|
||||
// Above this gap we rebuild the feed from the backend rather than fetch every
|
||||
// missing block one by one (matches the former LpIndexerService threshold).
|
||||
constexpr quint64 kGapRebuildThreshold = 8;
|
||||
constexpr int kPollIntervalMs = 2000;
|
||||
// Above this gap we rebuild the feed from the backend rather than fetch every
|
||||
// missing block one by one (matches the former LpIndexerService threshold).
|
||||
constexpr quint64 kGapRebuildThreshold = 8;
|
||||
|
||||
// Starter config shown in the Settings editor (and its "Reset to default").
|
||||
// A working local-dev indexer config; the user edits the fields (bedrock addr,
|
||||
// channel id, initial accounts/commitments, signing key, ...) and saves.
|
||||
const char* const kDefaultConfig = R"JSON({
|
||||
// Map the indexer FFI's OperationStatus to a human message.
|
||||
QString startErrorMessage(qlonglong code) {
|
||||
switch (code) {
|
||||
case 2: // OperationStatus::InitializationError
|
||||
return QStringLiteral(
|
||||
"Indexer failed to initialize (InitializationError). This is usually a "
|
||||
"storage problem — the data directory is locked by another running "
|
||||
"indexer, or isn't writable — or an invalid config field. It is not "
|
||||
"necessarily the JSON you just edited."
|
||||
);
|
||||
case 3: // OperationStatus::ClientError
|
||||
return QStringLiteral(
|
||||
"Indexer client error (ClientError) while starting — check the bedrock "
|
||||
"node address in the config and that the node is reachable."
|
||||
);
|
||||
case 1: // OperationStatus::NullPointer
|
||||
return QStringLiteral("Indexer failed to start (NullPointer).");
|
||||
default:
|
||||
return QStringLiteral("Indexer failed to start (OperationStatus %1).").arg(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Starter config shown in the Settings editor (and its "Reset to default").
|
||||
// A working local-dev indexer config; the user edits the fields (bedrock addr,
|
||||
// channel id, initial accounts/commitments, signing key, ...) and saves.
|
||||
const char* const kDefaultConfig = R"JSON({
|
||||
"consensus_info_polling_interval": "1s",
|
||||
"bedrock_config": {
|
||||
"addr": "http://localhost:18080",
|
||||
"addr": "http://localhost:8080",
|
||||
"backoff": {
|
||||
"start_delay": "100ms",
|
||||
"max_retries": 5
|
||||
}
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"initial_accounts": [
|
||||
{ "account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r", "balance": 10000 },
|
||||
{ "account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2", "balance": 20000 }
|
||||
],
|
||||
"initial_commitments": [],
|
||||
"signing_key": [
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37
|
||||
]
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101"
|
||||
})JSON";
|
||||
|
||||
// 64/128-bit values arrive as decimal strings (to dodge JSON double precision
|
||||
// loss), but be lenient and also accept a JSON number.
|
||||
quint64 jsonU64(const QJsonValue& value)
|
||||
{
|
||||
if (value.isString()) {
|
||||
bool ok = false;
|
||||
const qulonglong parsed = value.toString().toULongLong(&ok);
|
||||
return ok ? parsed : 0;
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
return number > 0.0 ? static_cast<quint64>(number) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString jsonStr(const QJsonValue& value)
|
||||
{
|
||||
if (value.isString()) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
return QString::number(static_cast<qulonglong>(value.toDouble()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Account ids are Base58 (matching the wallet UI and canonical LEZ); block/tx
|
||||
// hashes are hex. These mirror the indexer module's codec so the explorer can
|
||||
// recognise an account-shaped query and always display the canonical Base58 form.
|
||||
const char* const kBase58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
int base58Index(char c)
|
||||
{
|
||||
for (int i = 0; i < 58; ++i) {
|
||||
if (kBase58Alphabet[i] == c) {
|
||||
return i;
|
||||
// 64/128-bit values arrive as decimal strings (to dodge JSON double precision
|
||||
// loss), but be lenient and also accept a JSON number.
|
||||
quint64 jsonU64(const QJsonValue& value) {
|
||||
if (value.isString()) {
|
||||
bool ok = false;
|
||||
const qulonglong parsed = value.toString().toULongLong(&ok);
|
||||
return ok ? parsed : 0;
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
return number > 0.0 ? static_cast<quint64>(number) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Decode a Base58 string (plain Bitcoin alphabet, no checksum) into raw bytes.
|
||||
// Returns false on any non-Base58 character. Leading '1's map to leading zeros.
|
||||
bool base58Decode(const QString& s, QByteArray& out)
|
||||
{
|
||||
const QString t = s.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return false;
|
||||
QString jsonStr(const QJsonValue& value) {
|
||||
if (value.isString()) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
return QString::number(static_cast<qulonglong>(value.toDouble()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
int leadingZeros = 0;
|
||||
for (int i = 0; i < t.size() && t.at(i) == QLatin1Char('1'); ++i) {
|
||||
++leadingZeros;
|
||||
|
||||
// Account ids are Base58 (matching the wallet UI and canonical LEZ); block/tx
|
||||
// hashes are hex. These mirror the indexer module's codec so the explorer can
|
||||
// recognise an account-shaped query and always display the canonical Base58
|
||||
// form.
|
||||
const char* const kBase58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
int base58Index(char c) {
|
||||
for (int i = 0; i < 58; ++i) {
|
||||
if (kBase58Alphabet[i] == c) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
QByteArray digits; // base-256, least-significant first
|
||||
for (const QChar qc : t) {
|
||||
const int idx = qc.unicode() < 128 ? base58Index(static_cast<char>(qc.unicode())) : -1;
|
||||
if (idx < 0) {
|
||||
|
||||
// Decode a Base58 string (plain Bitcoin alphabet, no checksum) into raw bytes.
|
||||
// Returns false on any non-Base58 character. Leading '1's map to leading zeros.
|
||||
bool base58Decode(const QString& s, QByteArray& out) {
|
||||
const QString t = s.trimmed();
|
||||
if (t.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int carry = idx;
|
||||
for (int j = 0; j < digits.size(); ++j) {
|
||||
carry += static_cast<unsigned char>(digits[j]) * 58;
|
||||
digits[j] = static_cast<char>(carry & 0xFF);
|
||||
carry >>= 8;
|
||||
int leadingZeros = 0;
|
||||
for (int i = 0; i < t.size() && t.at(i) == QLatin1Char('1'); ++i) {
|
||||
++leadingZeros;
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.append(static_cast<char>(carry & 0xFF));
|
||||
carry >>= 8;
|
||||
QByteArray digits; // base-256, least-significant first
|
||||
for (const QChar qc : t) {
|
||||
const int idx = qc.unicode() < 128 ? base58Index(static_cast<char>(qc.unicode())) : -1;
|
||||
if (idx < 0) {
|
||||
return false;
|
||||
}
|
||||
int carry = idx;
|
||||
for (int j = 0; j < digits.size(); ++j) {
|
||||
carry += static_cast<unsigned char>(digits[j]) * 58;
|
||||
digits[j] = static_cast<char>(carry & 0xFF);
|
||||
carry >>= 8;
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.append(static_cast<char>(carry & 0xFF));
|
||||
carry >>= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
out = QByteArray(leadingZeros, '\0');
|
||||
for (int i = digits.size(); i-- > 0;) {
|
||||
out.append(digits[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Base58-encode raw bytes (plain Bitcoin alphabet). Inverse of base58Decode.
|
||||
QString bytes32ToBase58(const QByteArray& data)
|
||||
{
|
||||
int leadingZeros = 0;
|
||||
while (leadingZeros < data.size() && data[leadingZeros] == '\0') {
|
||||
++leadingZeros;
|
||||
}
|
||||
QByteArray digits; // base-58, least-significant first
|
||||
for (int i = 0; i < data.size(); ++i) {
|
||||
int carry = static_cast<unsigned char>(data[i]);
|
||||
for (int j = 0; j < digits.size(); ++j) {
|
||||
carry += static_cast<unsigned char>(digits[j]) * 256;
|
||||
digits[j] = static_cast<char>(carry % 58);
|
||||
carry /= 58;
|
||||
out = QByteArray(leadingZeros, '\0');
|
||||
for (int i = digits.size(); i-- > 0;) {
|
||||
out.append(digits[i]);
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.append(static_cast<char>(carry % 58));
|
||||
carry /= 58;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Base58-encode raw bytes (plain Bitcoin alphabet). Inverse of base58Decode.
|
||||
QString bytes32ToBase58(const QByteArray& data) {
|
||||
int leadingZeros = 0;
|
||||
while (leadingZeros < data.size() && data[leadingZeros] == '\0') {
|
||||
++leadingZeros;
|
||||
}
|
||||
QByteArray digits; // base-58, least-significant first
|
||||
for (int i = 0; i < data.size(); ++i) {
|
||||
int carry = static_cast<unsigned char>(data[i]);
|
||||
for (int j = 0; j < digits.size(); ++j) {
|
||||
carry += static_cast<unsigned char>(digits[j]) * 256;
|
||||
digits[j] = static_cast<char>(carry % 58);
|
||||
carry /= 58;
|
||||
}
|
||||
while (carry > 0) {
|
||||
digits.append(static_cast<char>(carry % 58));
|
||||
carry /= 58;
|
||||
}
|
||||
}
|
||||
QString out(leadingZeros, QLatin1Char('1'));
|
||||
for (int i = digits.size(); i-- > 0;) {
|
||||
out.append(QLatin1Char(kBase58Alphabet[static_cast<unsigned char>(digits[i])]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
QString out(leadingZeros, QLatin1Char('1'));
|
||||
for (int i = digits.size(); i-- > 0;) {
|
||||
out.append(QLatin1Char(kBase58Alphabet[static_cast<unsigned char>(digits[i])]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Canonical Base58 form of an account-id query, or "" if not account-shaped.
|
||||
// Accepts Base58 (must decode to 32 bytes) or 64-char hex (optionally 0x): a hex
|
||||
// search is normalised to Base58 so the result is displayed in canonical form.
|
||||
QString canonicalAccountId(const QString& query)
|
||||
{
|
||||
const QString trimmed = query.trimmed();
|
||||
QByteArray decoded;
|
||||
if (base58Decode(trimmed, decoded) && decoded.size() == 32) {
|
||||
return trimmed;
|
||||
// Canonical Base58 form of an account-id query, or "" if not account-shaped.
|
||||
// Accepts Base58 (must decode to 32 bytes) or 64-char hex (optionally 0x): a
|
||||
// hex search is normalised to Base58 so the result is displayed in canonical
|
||||
// form.
|
||||
QString canonicalAccountId(const QString& query) {
|
||||
const QString trimmed = query.trimmed();
|
||||
QByteArray decoded;
|
||||
if (base58Decode(trimmed, decoded) && decoded.size() == 32) {
|
||||
return trimmed;
|
||||
}
|
||||
QString hex = trimmed;
|
||||
if (hex.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
|
||||
hex = hex.mid(2);
|
||||
}
|
||||
static const QRegularExpression hexRe(QStringLiteral("^[0-9a-fA-F]{64}$"));
|
||||
if (hexRe.match(hex).hasMatch()) {
|
||||
return bytes32ToBase58(QByteArray::fromHex(hex.toLatin1()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
QString hex = trimmed;
|
||||
if (hex.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
|
||||
hex = hex.mid(2);
|
||||
}
|
||||
static const QRegularExpression hexRe(QStringLiteral("^[0-9a-fA-F]{64}$"));
|
||||
if (hexRe.match(hex).hasMatch()) {
|
||||
return bytes32ToBase58(QByteArray::fromHex(hex.toLatin1()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QString timestampText(quint64 timestamp)
|
||||
{
|
||||
QDateTime dt;
|
||||
// Tolerate either seconds or milliseconds since epoch.
|
||||
if (timestamp > 1000000000000ULL) {
|
||||
dt = QDateTime::fromMSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
} else {
|
||||
dt = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
QString timestampText(quint64 timestamp) {
|
||||
QDateTime dt;
|
||||
// Tolerate either seconds or milliseconds since epoch.
|
||||
if (timestamp > 1000000000000ULL) {
|
||||
dt = QDateTime::fromMSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
} else {
|
||||
dt = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
}
|
||||
if (!dt.isValid()) {
|
||||
dt = QDateTime::currentDateTimeUtc();
|
||||
}
|
||||
return dt.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")) + QStringLiteral(" UTC");
|
||||
}
|
||||
if (!dt.isValid()) {
|
||||
dt = QDateTime::currentDateTimeUtc();
|
||||
}
|
||||
return dt.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")) + QStringLiteral(" UTC");
|
||||
}
|
||||
|
||||
QVariantMap txObjectToMap(const QJsonObject& obj)
|
||||
{
|
||||
QVariantMap tx;
|
||||
if (obj.isEmpty()) {
|
||||
QVariantMap txObjectToMap(const QJsonObject& obj) {
|
||||
QVariantMap tx;
|
||||
if (obj.isEmpty()) {
|
||||
return tx;
|
||||
}
|
||||
|
||||
tx.insert(QStringLiteral("hash"), jsonStr(obj.value(QStringLiteral("hash"))));
|
||||
const QString type = obj.value(QStringLiteral("type")).toString();
|
||||
tx.insert(QStringLiteral("type"), type);
|
||||
|
||||
const auto accountsOf = [](const QJsonArray& arr) {
|
||||
QVariantList accounts;
|
||||
for (const auto& entry : arr) {
|
||||
const QJsonObject account = entry.toObject();
|
||||
QVariantMap ref;
|
||||
ref.insert(QStringLiteral("accountId"), jsonStr(account.value(QStringLiteral("account_id"))));
|
||||
QString nonce = jsonStr(account.value(QStringLiteral("nonce")));
|
||||
ref.insert(QStringLiteral("nonce"), nonce.isEmpty() ? QStringLiteral("0") : nonce);
|
||||
accounts.append(ref);
|
||||
}
|
||||
return accounts;
|
||||
};
|
||||
|
||||
if (type == QLatin1String("Public")) {
|
||||
tx.insert(QStringLiteral("programId"), jsonStr(obj.value(QStringLiteral("program_id"))));
|
||||
tx.insert(QStringLiteral("accounts"), accountsOf(obj.value(QStringLiteral("accounts")).toArray()));
|
||||
tx.insert(
|
||||
QStringLiteral("instructionDataCount"), obj.value(QStringLiteral("instruction_data")).toArray().size()
|
||||
);
|
||||
tx.insert(QStringLiteral("signatureCount"), obj.value(QStringLiteral("signature_count")).toInt());
|
||||
} else if (type == QLatin1String("PrivacyPreserving")) {
|
||||
tx.insert(QStringLiteral("accounts"), accountsOf(obj.value(QStringLiteral("accounts")).toArray()));
|
||||
tx.insert(
|
||||
QStringLiteral("newCommitmentsCount"), obj.value(QStringLiteral("new_commitments_count")).toInt()
|
||||
);
|
||||
tx.insert(QStringLiteral("nullifiersCount"), obj.value(QStringLiteral("nullifiers_count")).toInt());
|
||||
tx.insert(
|
||||
QStringLiteral("encryptedStatesCount"), obj.value(QStringLiteral("encrypted_states_count")).toInt()
|
||||
);
|
||||
tx.insert(
|
||||
QStringLiteral("validityWindowStart"),
|
||||
static_cast<qlonglong>(jsonU64(obj.value(QStringLiteral("validity_window_start"))))
|
||||
);
|
||||
tx.insert(
|
||||
QStringLiteral("validityWindowEnd"),
|
||||
static_cast<qlonglong>(jsonU64(obj.value(QStringLiteral("validity_window_end"))))
|
||||
);
|
||||
tx.insert(QStringLiteral("signatureCount"), obj.value(QStringLiteral("signature_count")).toInt());
|
||||
tx.insert(QStringLiteral("proofSizeBytes"), obj.value(QStringLiteral("proof_size")).toInt());
|
||||
} else if (type == QLatin1String("ProgramDeployment")) {
|
||||
tx.insert(QStringLiteral("bytecodeSizeBytes"), obj.value(QStringLiteral("bytecode_size")).toInt());
|
||||
}
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
tx.insert(QStringLiteral("hash"), jsonStr(obj.value(QStringLiteral("hash"))));
|
||||
const QString type = obj.value(QStringLiteral("type")).toString();
|
||||
tx.insert(QStringLiteral("type"), type);
|
||||
|
||||
const auto accountsOf = [](const QJsonArray& arr) {
|
||||
QVariantList accounts;
|
||||
for (const auto& entry : arr) {
|
||||
const QJsonObject account = entry.toObject();
|
||||
QVariantMap ref;
|
||||
ref.insert(QStringLiteral("accountId"), jsonStr(account.value(QStringLiteral("account_id"))));
|
||||
QString nonce = jsonStr(account.value(QStringLiteral("nonce")));
|
||||
ref.insert(QStringLiteral("nonce"), nonce.isEmpty() ? QStringLiteral("0") : nonce);
|
||||
accounts.append(ref);
|
||||
QVariantMap blockObjectToMap(const QJsonObject& obj) {
|
||||
QVariantMap block;
|
||||
if (obj.isEmpty()) {
|
||||
return block;
|
||||
}
|
||||
return accounts;
|
||||
};
|
||||
|
||||
if (type == QLatin1String("Public")) {
|
||||
tx.insert(QStringLiteral("programId"), jsonStr(obj.value(QStringLiteral("program_id"))));
|
||||
tx.insert(QStringLiteral("accounts"), accountsOf(obj.value(QStringLiteral("accounts")).toArray()));
|
||||
tx.insert(QStringLiteral("instructionDataCount"),
|
||||
obj.value(QStringLiteral("instruction_data")).toArray().size());
|
||||
tx.insert(QStringLiteral("signatureCount"), obj.value(QStringLiteral("signature_count")).toInt());
|
||||
} else if (type == QLatin1String("PrivacyPreserving")) {
|
||||
tx.insert(QStringLiteral("accounts"), accountsOf(obj.value(QStringLiteral("accounts")).toArray()));
|
||||
tx.insert(QStringLiteral("newCommitmentsCount"), obj.value(QStringLiteral("new_commitments_count")).toInt());
|
||||
tx.insert(QStringLiteral("nullifiersCount"), obj.value(QStringLiteral("nullifiers_count")).toInt());
|
||||
tx.insert(QStringLiteral("encryptedStatesCount"), obj.value(QStringLiteral("encrypted_states_count")).toInt());
|
||||
tx.insert(QStringLiteral("validityWindowStart"),
|
||||
static_cast<qlonglong>(jsonU64(obj.value(QStringLiteral("validity_window_start")))));
|
||||
tx.insert(QStringLiteral("validityWindowEnd"),
|
||||
static_cast<qlonglong>(jsonU64(obj.value(QStringLiteral("validity_window_end")))));
|
||||
tx.insert(QStringLiteral("signatureCount"), obj.value(QStringLiteral("signature_count")).toInt());
|
||||
tx.insert(QStringLiteral("proofSizeBytes"), obj.value(QStringLiteral("proof_size")).toInt());
|
||||
} else if (type == QLatin1String("ProgramDeployment")) {
|
||||
tx.insert(QStringLiteral("bytecodeSizeBytes"), obj.value(QStringLiteral("bytecode_size")).toInt());
|
||||
}
|
||||
block.insert(QStringLiteral("blockId"), static_cast<qlonglong>(jsonU64(obj.value(QStringLiteral("block_id")))));
|
||||
block.insert(QStringLiteral("hash"), jsonStr(obj.value(QStringLiteral("hash"))));
|
||||
block.insert(QStringLiteral("prevBlockHash"), jsonStr(obj.value(QStringLiteral("prev_block_hash"))));
|
||||
block.insert(QStringLiteral("signature"), jsonStr(obj.value(QStringLiteral("signature"))));
|
||||
block.insert(QStringLiteral("timestampText"), timestampText(jsonU64(obj.value(QStringLiteral("timestamp")))));
|
||||
|
||||
return tx;
|
||||
}
|
||||
const QString status = obj.value(QStringLiteral("bedrock_status")).toString(QStringLiteral("Pending"));
|
||||
block.insert(QStringLiteral("status"), status);
|
||||
|
||||
const QJsonArray txArray = obj.value(QStringLiteral("transactions")).toArray();
|
||||
QVariantList transactions;
|
||||
transactions.reserve(txArray.size());
|
||||
for (const auto& txValue : txArray) {
|
||||
const QVariantMap tx = txObjectToMap(txValue.toObject());
|
||||
if (!tx.isEmpty()) {
|
||||
transactions.append(tx);
|
||||
}
|
||||
}
|
||||
block.insert(QStringLiteral("transactions"), transactions);
|
||||
block.insert(QStringLiteral("txCount"), transactions.size());
|
||||
|
||||
QVariantMap blockObjectToMap(const QJsonObject& obj)
|
||||
{
|
||||
QVariantMap block;
|
||||
if (obj.isEmpty()) {
|
||||
return block;
|
||||
}
|
||||
|
||||
block.insert(QStringLiteral("blockId"), static_cast<qlonglong>(jsonU64(obj.value(QStringLiteral("block_id")))));
|
||||
block.insert(QStringLiteral("hash"), jsonStr(obj.value(QStringLiteral("hash"))));
|
||||
block.insert(QStringLiteral("prevBlockHash"), jsonStr(obj.value(QStringLiteral("prev_block_hash"))));
|
||||
block.insert(QStringLiteral("signature"), jsonStr(obj.value(QStringLiteral("signature"))));
|
||||
block.insert(QStringLiteral("timestampText"), timestampText(jsonU64(obj.value(QStringLiteral("timestamp")))));
|
||||
|
||||
const QString status = obj.value(QStringLiteral("bedrock_status")).toString(QStringLiteral("Pending"));
|
||||
block.insert(QStringLiteral("status"), status);
|
||||
|
||||
const QJsonArray txArray = obj.value(QStringLiteral("transactions")).toArray();
|
||||
QVariantList transactions;
|
||||
transactions.reserve(txArray.size());
|
||||
for (const auto& txValue : txArray) {
|
||||
const QVariantMap tx = txObjectToMap(txValue.toObject());
|
||||
if (!tx.isEmpty()) {
|
||||
transactions.append(tx);
|
||||
QVariantMap blockJsonToMap(const QString& json) {
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
return doc.isObject() ? blockObjectToMap(doc.object()) : QVariantMap();
|
||||
}
|
||||
block.insert(QStringLiteral("transactions"), transactions);
|
||||
block.insert(QStringLiteral("txCount"), transactions.size());
|
||||
|
||||
return block;
|
||||
}
|
||||
QVariantMap accountJsonToMap(const QString& accountId, const QString& json) {
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return {};
|
||||
}
|
||||
const QJsonObject obj = doc.object();
|
||||
|
||||
QVariantMap blockJsonToMap(const QString& json)
|
||||
{
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
QVariantMap account;
|
||||
account.insert(QStringLiteral("accountId"),
|
||||
accountId); // payload omits it; inject the queried id
|
||||
account.insert(QStringLiteral("programOwner"), jsonStr(obj.value(QStringLiteral("program_owner"))));
|
||||
QString balance = jsonStr(obj.value(QStringLiteral("balance")));
|
||||
account.insert(QStringLiteral("balance"), balance.isEmpty() ? QStringLiteral("0") : balance);
|
||||
QString nonce = jsonStr(obj.value(QStringLiteral("nonce")));
|
||||
account.insert(QStringLiteral("nonce"), nonce.isEmpty() ? QStringLiteral("0") : nonce);
|
||||
account.insert(QStringLiteral("dataSizeBytes"), obj.value(QStringLiteral("data_size")).toInt());
|
||||
return account;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
return doc.isObject() ? blockObjectToMap(doc.object()) : QVariantMap();
|
||||
}
|
||||
|
||||
QVariantMap accountJsonToMap(const QString& accountId, const QString& json)
|
||||
{
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
QVariantMap txJsonToMap(const QString& json) {
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
return doc.isObject() ? txObjectToMap(doc.object()) : QVariantMap();
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return {};
|
||||
}
|
||||
const QJsonObject obj = doc.object();
|
||||
|
||||
QVariantMap account;
|
||||
account.insert(QStringLiteral("accountId"), accountId); // payload omits it; inject the queried id
|
||||
account.insert(QStringLiteral("programOwner"), jsonStr(obj.value(QStringLiteral("program_owner"))));
|
||||
QString balance = jsonStr(obj.value(QStringLiteral("balance")));
|
||||
account.insert(QStringLiteral("balance"), balance.isEmpty() ? QStringLiteral("0") : balance);
|
||||
QString nonce = jsonStr(obj.value(QStringLiteral("nonce")));
|
||||
account.insert(QStringLiteral("nonce"), nonce.isEmpty() ? QStringLiteral("0") : nonce);
|
||||
account.insert(QStringLiteral("dataSizeBytes"), obj.value(QStringLiteral("data_size")).toInt());
|
||||
return account;
|
||||
}
|
||||
|
||||
QVariantMap txJsonToMap(const QString& json)
|
||||
{
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
return doc.isObject() ? txObjectToMap(doc.object()) : QVariantMap();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LezExplorerUiBackend::LezExplorerUiBackend()
|
||||
{
|
||||
LezExplorerUiBackend::LezExplorerUiBackend() {
|
||||
setDefaultConfig(QString::fromUtf8(kDefaultConfig));
|
||||
}
|
||||
|
||||
LezExplorerUiBackend::~LezExplorerUiBackend()
|
||||
{
|
||||
LezExplorerUiBackend::~LezExplorerUiBackend() {
|
||||
// Defensive: ensure no poll fires while the backend is being torn down.
|
||||
if (m_pollTimer) {
|
||||
m_pollTimer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void LezExplorerUiBackend::onContextReady()
|
||||
{
|
||||
void LezExplorerUiBackend::onContextReady() {
|
||||
setConnectionStatus(QStringLiteral("Connecting"));
|
||||
|
||||
// Reload + auto-start the previously-saved config, if any (set-once UX).
|
||||
@ -360,8 +370,7 @@ void LezExplorerUiBackend::onContextReady()
|
||||
pollTip();
|
||||
}
|
||||
|
||||
bool LezExplorerUiBackend::applyConfigJson(QString json)
|
||||
{
|
||||
bool LezExplorerUiBackend::applyConfigJson(QString json) {
|
||||
const QString trimmed = json.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
emit error(QStringLiteral("Config is empty."));
|
||||
@ -390,8 +399,7 @@ bool LezExplorerUiBackend::applyConfigJson(QString json)
|
||||
return ok;
|
||||
}
|
||||
|
||||
void LezExplorerUiBackend::refreshBlocks()
|
||||
{
|
||||
void LezExplorerUiBackend::refreshBlocks() {
|
||||
const QString json = modules().lez_indexer_module.getBlocks(QString(), QStringLiteral("10"));
|
||||
m_recentBlocks = parseBlockArrayJson(json);
|
||||
|
||||
@ -409,13 +417,12 @@ void LezExplorerUiBackend::refreshBlocks()
|
||||
setRecentBlocks(m_recentBlocks);
|
||||
}
|
||||
|
||||
void LezExplorerUiBackend::loadMoreBlocks()
|
||||
{
|
||||
void LezExplorerUiBackend::loadMoreBlocks() {
|
||||
if (m_oldestLoadedId <= 1) {
|
||||
return; // already at genesis
|
||||
}
|
||||
const QString json
|
||||
= modules().lez_indexer_module.getBlocks(QString::number(m_oldestLoadedId), QStringLiteral("10"));
|
||||
const QString json =
|
||||
modules().lez_indexer_module.getBlocks(QString::number(m_oldestLoadedId), QStringLiteral("10"));
|
||||
const QVariantList older = parseBlockArrayJson(json);
|
||||
if (older.isEmpty()) {
|
||||
return;
|
||||
@ -431,30 +438,26 @@ void LezExplorerUiBackend::loadMoreBlocks()
|
||||
setRecentBlocks(m_recentBlocks);
|
||||
}
|
||||
|
||||
QVariantMap LezExplorerUiBackend::getBlockById(QString id)
|
||||
{
|
||||
QVariantMap LezExplorerUiBackend::getBlockById(QString id) {
|
||||
return blockJsonToMap(modules().lez_indexer_module.getBlockById(id));
|
||||
}
|
||||
|
||||
QVariantMap LezExplorerUiBackend::getBlockByHash(QString hash)
|
||||
{
|
||||
QVariantMap LezExplorerUiBackend::getBlockByHash(QString hash) {
|
||||
return blockJsonToMap(modules().lez_indexer_module.getBlockByHash(hash));
|
||||
}
|
||||
|
||||
QVariantMap LezExplorerUiBackend::getTransaction(QString hash)
|
||||
{
|
||||
QVariantMap LezExplorerUiBackend::getTransaction(QString hash) {
|
||||
return txJsonToMap(modules().lez_indexer_module.getTransaction(hash));
|
||||
}
|
||||
|
||||
QVariantMap LezExplorerUiBackend::getAccount(QString accountId)
|
||||
{
|
||||
QVariantMap LezExplorerUiBackend::getAccount(QString accountId) {
|
||||
return accountJsonToMap(accountId, modules().lez_indexer_module.getAccount(accountId));
|
||||
}
|
||||
|
||||
QVariantList LezExplorerUiBackend::getTransactionsByAccount(QString accountId, int offset, int limit)
|
||||
{
|
||||
QVariantList LezExplorerUiBackend::getTransactionsByAccount(QString accountId, int offset, int limit) {
|
||||
const QString json = modules().lez_indexer_module.getTransactionsByAccount(
|
||||
accountId, QString::number(offset), QString::number(limit));
|
||||
accountId, QString::number(offset), QString::number(limit)
|
||||
);
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
@ -475,8 +478,7 @@ QVariantList LezExplorerUiBackend::getTransactionsByAccount(QString accountId, i
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariantMap LezExplorerUiBackend::search(QString query)
|
||||
{
|
||||
QVariantMap LezExplorerUiBackend::search(QString query) {
|
||||
QVariantList blocks;
|
||||
QVariantList transactions;
|
||||
QVariantList accounts;
|
||||
@ -528,8 +530,7 @@ QVariantMap LezExplorerUiBackend::search(QString query)
|
||||
return results;
|
||||
}
|
||||
|
||||
quint64 LezExplorerUiBackend::applySyncStatus(const QString& json)
|
||||
{
|
||||
quint64 LezExplorerUiBackend::applySyncStatus(const QString& json) {
|
||||
// Empty JSON means the indexer isn't running (not configured / not started);
|
||||
// a real status always carries one of the core's states.
|
||||
QString state = QStringLiteral("stopped");
|
||||
@ -565,8 +566,7 @@ quint64 LezExplorerUiBackend::applySyncStatus(const QString& json)
|
||||
return blockId;
|
||||
}
|
||||
|
||||
void LezExplorerUiBackend::pollTip()
|
||||
{
|
||||
void LezExplorerUiBackend::pollTip() {
|
||||
// One status call drives both the sync banner and the feed: indexedBlockId
|
||||
// is the L2 tip we page from.
|
||||
const quint64 tip = applySyncStatus(modules().lez_indexer_module.getStatus());
|
||||
@ -614,13 +614,11 @@ void LezExplorerUiBackend::pollTip()
|
||||
setRecentBlocks(m_recentBlocks);
|
||||
}
|
||||
|
||||
QVariantMap LezExplorerUiBackend::fetchBlock(quint64 blockId)
|
||||
{
|
||||
QVariantMap LezExplorerUiBackend::fetchBlock(quint64 blockId) {
|
||||
return blockJsonToMap(modules().lez_indexer_module.getBlockById(QString::number(blockId)));
|
||||
}
|
||||
|
||||
QVariantList LezExplorerUiBackend::parseBlockArrayJson(const QString& json) const
|
||||
{
|
||||
QVariantList LezExplorerUiBackend::parseBlockArrayJson(const QString& json) const {
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
@ -642,29 +640,26 @@ QVariantList LezExplorerUiBackend::parseBlockArrayJson(const QString& json) cons
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The writable path where the edited config JSON is persisted and the indexer
|
||||
// is started from (so the UI never deals with paths). Same logic, shared by the
|
||||
// const and non-const accessors.
|
||||
QString resolveConfigFilePath()
|
||||
{
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
||||
if (dir.isEmpty()) {
|
||||
dir = QDir::tempPath();
|
||||
// The writable path where the edited config JSON is persisted and the indexer
|
||||
// is started from (so the UI never deals with paths). Same logic, shared by the
|
||||
// const and non-const accessors.
|
||||
QString resolveConfigFilePath() {
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
||||
if (dir.isEmpty()) {
|
||||
dir = QDir::tempPath();
|
||||
}
|
||||
return QDir(dir).filePath(QStringLiteral("indexer_config.json"));
|
||||
}
|
||||
return QDir(dir).filePath(QStringLiteral("indexer_config.json"));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QString LezExplorerUiBackend::configFilePath()
|
||||
{
|
||||
QString LezExplorerUiBackend::configFilePath() {
|
||||
if (m_configFilePath.isEmpty()) {
|
||||
m_configFilePath = resolveConfigFilePath();
|
||||
}
|
||||
return m_configFilePath;
|
||||
}
|
||||
|
||||
bool LezExplorerUiBackend::writeConfigFile(const QString& json)
|
||||
{
|
||||
bool LezExplorerUiBackend::writeConfigFile(const QString& json) {
|
||||
const QString path = configFilePath();
|
||||
const QDir dir = QFileInfo(path).dir();
|
||||
if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
|
||||
@ -680,8 +675,7 @@ bool LezExplorerUiBackend::writeConfigFile(const QString& json)
|
||||
return ok;
|
||||
}
|
||||
|
||||
QString LezExplorerUiBackend::readConfigFile() const
|
||||
{
|
||||
QString LezExplorerUiBackend::readConfigFile() const {
|
||||
QFile file(m_configFilePath.isEmpty() ? resolveConfigFilePath() : m_configFilePath);
|
||||
if (!file.exists() || !file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return {};
|
||||
@ -691,15 +685,15 @@ QString LezExplorerUiBackend::readConfigFile() const
|
||||
return content;
|
||||
}
|
||||
|
||||
bool LezExplorerUiBackend::startIndexerFromFile()
|
||||
{
|
||||
// stops the indexer if it's already running, then starts it with the config file path.
|
||||
bool LezExplorerUiBackend::startIndexerFromFile() {
|
||||
// stops the indexer if it's already running, then starts it with the config
|
||||
// file path.
|
||||
//
|
||||
// this has the effect of "restarting" the indexer with the new config.
|
||||
modules().lez_indexer_module.stop_indexer();
|
||||
const qlonglong code = modules().lez_indexer_module.start_indexer(configFilePath());
|
||||
if (code != 0) {
|
||||
emit error(QStringLiteral("start_indexer failed (code %1)").arg(code));
|
||||
emit error(startErrorMessage(code));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@ -145,9 +145,9 @@ Item {
|
||||
if (ok) {
|
||||
page.setStatus("Saved. Indexer starting…", false);
|
||||
page.explorer.goHome();
|
||||
} else {
|
||||
page.setStatus("Save failed — check the config and try again.", true);
|
||||
}
|
||||
// On failure the backend emits error() with a specific reason
|
||||
// (shown by onError above) — don't clobber it with a generic line.
|
||||
},
|
||||
function (err) {
|
||||
page.setStatus("Error: " + err, true);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user