From 85475fad6936c9e8b5c15da87bca7d6e989b51cb Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Fri, 14 Aug 2026 20:29:55 -0300 Subject: [PATCH] feat(token-ui): connect Basecamp UI to token module --- apps/common/wallet-ui/README.md | 94 - .../qml/Logos/Wallet/WalletControl.qml | 499 ---- .../Logos/Wallet/internal/AccountDelegate.qml | 84 - .../Wallet/internal/CreateAccountDialog.qml | 102 - .../Wallet/internal/CreateWalletDialog.qml | 201 -- .../Logos/Wallet/internal/LogosCopyButton.qml | 39 - .../Wallet/internal/WalletIconButton.qml | 25 - .../Logos/Wallet/internal/icons/account.svg | 1 - .../qml/Logos/Wallet/internal/icons/back.svg | 1 - .../Logos/Wallet/internal/icons/checkmark.svg | 1 - .../qml/Logos/Wallet/internal/icons/copy.svg | 1 - .../qml/Logos/Wallet/internal/icons/power.svg | 1 - .../Logos/Wallet/internal/icons/settings.svg | 1 - apps/common/wallet-ui/qml/Logos/Wallet/qmldir | 4 - apps/common/wallet-ui/src/AccountModel.cpp | 79 - apps/common/wallet-ui/src/AccountModel.h | 48 - .../common/wallet-ui/src/WalletBackendLogic.h | 453 ---- apps/token/CMakeLists.txt | 31 +- apps/token/README.md | 50 +- apps/token/flake.lock | 8 +- apps/token/flake.nix | 89 +- apps/token/metadata.json | 4 +- apps/token/qml/Logos/Wallet/qmldir | 5 + apps/token/qml/Main.qml | 22 +- apps/token/qml/NavBar.qml | 25 +- apps/token/qml/pages/CreatePage.qml | 2407 ++++++++++------- apps/token/qml/pages/ManagePage.qml | 207 +- ...TokenPrototypeStore.qml => TokenStore.qml} | 16 +- apps/token/src/TokenUiBackend.cpp | 453 +++- apps/token/src/TokenUiBackend.h | 85 +- apps/token/src/TokenUiBackend.rep | 29 +- flake.nix | 52 +- 32 files changed, 2232 insertions(+), 2885 deletions(-) delete mode 100644 apps/common/wallet-ui/README.md delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/WalletControl.qml delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/AccountDelegate.qml delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateAccountDialog.qml delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateWalletDialog.qml delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/LogosCopyButton.qml delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/WalletIconButton.qml delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/account.svg delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/back.svg delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/checkmark.svg delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/copy.svg delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/power.svg delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/settings.svg delete mode 100644 apps/common/wallet-ui/qml/Logos/Wallet/qmldir delete mode 100644 apps/common/wallet-ui/src/AccountModel.cpp delete mode 100644 apps/common/wallet-ui/src/AccountModel.h delete mode 100644 apps/common/wallet-ui/src/WalletBackendLogic.h create mode 100644 apps/token/qml/Logos/Wallet/qmldir rename apps/token/qml/state/{TokenPrototypeStore.qml => TokenStore.qml} (98%) diff --git a/apps/common/wallet-ui/README.md b/apps/common/wallet-ui/README.md deleted file mode 100644 index 51b8897..0000000 --- a/apps/common/wallet-ui/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Shared wallet UI - -Single source of truth for the wallet surface reused across LEZ QML apps -(`apps/amm`, `apps/token`, …): the account/settings navbar controls, the account -list model, and the wallet backend logic that talks to the core -`logos_execution_zone` module. - -This is a plain source tree, **not a flake**. Each app consumes it as a -non-flake path input, sharing it two ways (see any app's `flake.nix`): - -```nix -# apps//flake.nix -inputs.wallet_ui = { url = "path:../common/wallet-ui"; flake = false; }; -# C++ → a per-system runCommand overlays src/* into the app's build source. -# QML → the same runCommand overlays qml/Logos into the app's view dir, so the -# module ships at /qml/Logos/Wallet and NavBar imports it with -# `import "Logos/Wallet"` (relative). -``` - -## Why a relative import (not `import Logos.Wallet`) - -`Logos.Wallet` is structured as a proper QML module (a `qmldir` exporting only -`WalletControl`), but it is imported by **relative path** — `import -"Logos/Wallet"` — rather than by URI. The reason is the runtime: the LEZ -`ui-host` that hosts the view only searches its own baked QML import path -(`/lib` + Qt), and does **not** add the app's plugin dir. A bare -`import Logos.Wallet` URI therefore fails with *module "Logos.Wallet" is not -installed*, while a relative import (which resolves against the importing file's -directory, like `import "pages"`) works. Verified by running the app headlessly -against the real standalone/ui-host. - -PR #228 makes the URI form work by compiling the module with -`qt_add_qml_module(... RESOURCE_PREFIX /qt/qml)`, which embeds the QML as Qt -resources under `qrc:/qt/qml/Logos/Wallet` — a path Qt always searches — so it -does not depend on the app dir being on the import path. When that lands, each -app's `import "Logos/Wallet"` becomes `import Logos.Wallet` (one line), and the -overlay step is replaced by linking the compiled module. The directory layout -here already matches #228, so that swap is mechanical. - -## Contents - -| Path | What | Shared how | -| --- | --- | --- | -| `qml/Logos/Wallet/` | The importable **`Logos.Wallet`** QML module. Public: `WalletControl` (the navbar account/settings control). Private: `internal/*` (create-wallet / create-account dialogs, account delegate, icon buttons) — not listed in `qmldir`, so importers only see `WalletControl`. | Installed to `/lib/Logos/Wallet` via `postInstall`; used as `import Logos.Wallet` | -| `src/AccountModel.{h,cpp}` | `QAbstractListModel` of wallet accounts, exposed to QML via `logos.model("_ui", "accountModel")` | Overlaid into `/src/` at build | -| `src/WalletBackendLogic.h` | All wallet behaviour (open/adopt, account create, balances, sequencer settings, reachability) as a CRTP base | Overlaid into `/src/` at build | - -## How the QML is shared (`Logos.Wallet`) - -Rather than copying QML files into each app, the wallet UI is a real QML module -imported by URI — the same pattern as `Logos.Controls` / `Logos.Theme`. The -standalone/basecamp runtime puts the app's own plugin dir on the QML import -path, so an app that ships `lib/Logos/Wallet/qmldir` can simply: - -```qml -import Logos.Wallet -// ... -WalletControl { backend: ...; accountModel: ... } -``` - -The module boundary keeps the implementation controls private (`internal/`), -and mirrors PR #228's `Logos.Wallet` layout so its richer version (transaction -confirmation, submitted-transaction views, a `WalletProvider` abstraction) can -replace this module's contents without touching the consuming apps. - -## How the backend is shared - -Every app's backend derives from a QtRO `*SimpleSource` generated from its own -`UiBackend.rep`. Those `.rep` files are byte-identical except for the class -name, so the generated sources expose an identical property/slot surface. That -lets `WalletBackendLogic` inherit the generated source, reach its protected -PROP setters, and override its pure-virtual slots directly. A host backend is -then a near-empty shell: - -```cpp -class TokenUiBackend : public WalletBackendLogic { - Q_OBJECT - Q_PROPERTY(AccountModel* accountModel READ accountModel CONSTANT) -public: - explicit TokenUiBackend(LogosAPI* api = nullptr, QObject* parent = nullptr) - : WalletBackendLogic(api, parent, "token_ui", "TokenUI") {} -}; -``` - -The QML wallet components are program-agnostic: the host app passes its concrete -backend replica and account model in via the `backend`/`accountModel` -properties. - -## Editing - -Change these files once here; both apps pick the change up on their next build. -When adding a new wallet slot or PROP, update every app's `UiBackend.rep` -in lockstep (the surfaces must stay identical) and the CMake `SOURCES` lists if -you add files. diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/WalletControl.qml b/apps/common/wallet-ui/qml/Logos/Wallet/WalletControl.qml deleted file mode 100644 index 6ae5016..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/WalletControl.qml +++ /dev/null @@ -1,499 +0,0 @@ -import QtQuick -import QtQml -import QtQuick.Controls -import QtQuick.Layouts - -import Logos.Theme -import Logos.Controls - -// Private implementation controls (dialogs, delegates, icon buttons). Not part -// of the Logos.Wallet public API (see qmldir) — only WalletControl is exported. -import "internal" - -// Header wallet control (Uniswap-style), with two states: -// - not connected → a "Connect" button that opens the create-wallet modal -// - connected → a single button showing the active account address; -// clicking it opens a popup (top-right, just under the -// button) holding the account selector, create-account and -// disconnect actions. -// The selected account address is exposed via selectedAddress for the host -// app's flows to use as the "from" account. -// -// Shared across LEZ apps — see apps/common/wallet-ui. It is program-agnostic: -// the concrete backend replica (logos.module("_ui")) and account model -// are passed in by the host app via the `backend`/`accountModel` properties. -Item { - id: root - - // Backend replica (logos.module("_ui")) and its account model, - // supplied by the host app. - property var backend: null - property var accountModel: null - - readonly property bool connected: backend !== null && backend.isWalletOpen - - // Index of the active account. selectedAddress/selectedName are derived from - // the model mirror below so they stay valid while the popup (and its list) - // is closed. - property int selectedIndex: 0 - - // Non-visual mirror of the account model: realizes every row regardless of - // popup visibility, so the active account is addressable by index at all - // times (a ListView only realizes rows while it is shown). - Instantiator { - id: accounts - model: root.accountModel - delegate: QtObject { - readonly property string address: model.address ?? "" - readonly property string name: model.name ?? "" - readonly property string balance: model.balance ?? "" - readonly property bool isPublic: model.isPublic ?? false - } - } - - function entryAt(i) { - return (i >= 0 && i < accounts.count) ? accounts.objectAt(i) : null - } - - readonly property string selectedAddress: { - const e = root.entryAt(root.selectedIndex) - return e ? e.address : "" - } - readonly property string selectedName: { - const e = root.entryAt(root.selectedIndex) - return e ? e.name : "" - } - readonly property string selectedBalance: { - const e = root.entryAt(root.selectedIndex) - return e ? e.balance : "" - } - readonly property bool selectedIsPublic: { - const e = root.entryAt(root.selectedIndex) - return e ? e.isPublic : false - } - - // Keep the selection within bounds as accounts are added/removed. - function clampSelection() { - if (accounts.count === 0) { root.selectedIndex = 0; return } - if (root.selectedIndex < 0) root.selectedIndex = 0 - else if (root.selectedIndex >= accounts.count) root.selectedIndex = accounts.count - 1 - } - Connections { - target: root.accountModel - ignoreUnknownSignals: true - function onModelReset() { root.clampSelection() } - function onRowsInserted() { root.clampSelection() } - function onRowsRemoved() { root.clampSelection() } - } - - // 0x123456…cdef style truncation for the connected button label. - function truncated(addr) { - if (!addr) return "" - return addr.length > 13 ? (addr.substring(0, 6) + "…" + addr.substring(addr.length - 4)) : addr - } - - // Copy on the QML/view side. Routing this through the backend would call - // QGuiApplication::clipboard() in the (headless) module host process, which - // has no clipboard — that call tears the backend down, dropping the wallet - // connection. A hidden TextEdit copies via the GUI process that owns it. - TextEdit { id: clipboardProxy; visible: false } - function copyToClipboard(text) { - if (!text) return - clipboardProxy.text = text - clipboardProxy.selectAll() - clipboardProxy.copy() - clipboardProxy.deselect() - clipboardProxy.text = "" - } - - implicitWidth: root.connected ? connectedButton.width : connectButton.width - implicitHeight: 40 - - // ── Disconnected: Connect ──────────────────────────────────────────── - LogosButton { - id: connectButton - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - height: 40 - visible: !root.connected - enabled: root.backend !== null - text: qsTr("Connect") - onClicked: { - // Re-open an existing wallet; only show the create modal on first run. - if (root.backend && root.backend.walletExists) - logos.watch(root.backend.openExisting(), - function(ok) { if (!ok) console.warn("openExisting failed") }, - function(error) { console.warn("openExisting error:", error) }) - else - createWalletDialog.open() - } - } - - // ── Connected: address pill that toggles the wallet menu ───────────── - Rectangle { - id: connectedButton - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - visible: root.connected - implicitHeight: 40 - implicitWidth: connectedRow.implicitWidth + Theme.spacing.medium * 2 - radius: height / 2 - // Keep an opaque dark fill in both states: the navbar is white, and the - // active "muted" fill is translucent gray, which renders light over white - // and makes the white label unreadable. Signal "open" with an accent - // border instead. - color: Theme.palette.backgroundSecondary - border.width: 1 - border.color: walletMenu.opened ? Theme.palette.overlayOrange : "transparent" - - RowLayout { - id: connectedRow - anchors.centerIn: parent - spacing: Theme.spacing.small - - Rectangle { - Layout.preferredWidth: 8 - Layout.preferredHeight: 8 - radius: 4 - color: "#39c06a" - } - LogosText { - text: root.truncated(root.selectedAddress) || qsTr("Connected") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.text - } - LogosText { - text: walletMenu.opened ? "▴" : "▾" - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - // CloseOnPressOutside already dismisses the popup on this same press - // (the button is outside it), so `opened` is false by the time this - // fires. Without the recency guard the dismissing click would just - // reopen it. If it just closed, leave it closed. - onClicked: { - if (walletMenu.opened || (Date.now() - walletMenu.lastClosedMs) < 200) - walletMenu.close() - else - walletMenu.open() - } - } - } - - // ── Wallet menu popup (top-right, under the connected button) ───────── - Popup { - id: walletMenu - parent: connectedButton - y: connectedButton.height + Theme.spacing.small - x: connectedButton.width - width // right-align under the button - width: 360 - padding: Theme.spacing.medium - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - // Timestamp of the last dismissal, used by the toggle button to tell a - // genuine "open" click from the press that just closed the popup. - property real lastClosedMs: 0 - onClosed: { - walletMenu.lastClosedMs = Date.now() - // Always reopen on the main (selected-account) view. - if (viewStack.depth > 1) - viewStack.pop(null, StackView.Immediate) - } - - background: Rectangle { - color: Theme.palette.backgroundTertiary - border.width: 1 - border.color: Theme.palette.backgroundElevated - radius: Theme.spacing.radiusLarge - } - - // Two stacked views: the main view (active account + actions) and the - // accounts view (full list + create). The popup height follows the - // active view's natural height, animated so the resize isn't abrupt. - contentItem: StackView { - id: viewStack - clip: true - implicitWidth: walletMenu.availableWidth - implicitHeight: currentItem ? currentItem.implicitHeight : 0 - initialItem: mainView - - Behavior on implicitHeight { - NumberAnimation { duration: 160; easing.type: Easing.OutCubic } - } - - pushEnter: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 120 } } - pushExit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 120 } } - popEnter: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 120 } } - popExit: Transition { NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 120 } } - } - - // ── Main view: account icon + power icon, then the active account ── - Component { - id: mainView - - ColumnLayout { - spacing: Theme.spacing.medium - - // Top-right actions: open the account list / disconnect. - RowLayout { - Layout.fillWidth: true - spacing: Theme.spacing.small - - Item { Layout.fillWidth: true } - - WalletIconButton { - iconSource: Qt.resolvedUrl("internal/icons/account.svg") - onClicked: viewStack.push(accountsView) - } - WalletIconButton { - iconSource: Qt.resolvedUrl("internal/icons/settings.svg") - onClicked: viewStack.push(settingsView) - } - WalletIconButton { - iconSource: Qt.resolvedUrl("internal/icons/power.svg") - onClicked: { - walletMenu.close() - if (root.backend) root.backend.disconnectWallet() - } - } - } - - // Active account card. - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: cardColumn.implicitHeight + Theme.spacing.medium * 2 - radius: Theme.spacing.radiusLarge - color: Theme.palette.backgroundMuted - - ColumnLayout { - id: cardColumn - anchors.fill: parent - anchors.margins: Theme.spacing.medium - spacing: Theme.spacing.small - - RowLayout { - Layout.fillWidth: true - spacing: Theme.spacing.small - - LogosText { - text: root.selectedName - font.pixelSize: Theme.typography.secondaryText - font.bold: true - } - Rectangle { - Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2 - Layout.preferredHeight: tagLabel.implicitHeight + 4 - radius: 4 - color: Theme.palette.backgroundSecondary - LogosText { - id: tagLabel - anchors.centerIn: parent - text: root.selectedIsPublic ? qsTr("Public") : qsTr("Private") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - } - } - Item { Layout.fillWidth: true } - LogosText { - text: root.selectedBalance.length > 0 ? root.selectedBalance : "—" - font.bold: true - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 0 - LogosText { - Layout.fillWidth: true - verticalAlignment: Text.AlignVCenter - text: root.selectedAddress - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textMuted - elide: Text.ElideMiddle - } - LogosCopyButton { - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - visible: root.selectedAddress.length > 0 - onCopyText: root.copyToClipboard(root.selectedAddress) - icon.color: Theme.palette.textMuted - } - } - } - } - } - } - - // ── Accounts view: back + full list + create ────────────────────── - Component { - id: accountsView - - ColumnLayout { - spacing: Theme.spacing.medium - - // Header: back to the main view + title. - RowLayout { - Layout.fillWidth: true - spacing: Theme.spacing.small - - WalletIconButton { - iconSource: Qt.resolvedUrl("internal/icons/back.svg") - onClicked: viewStack.pop() - } - LogosText { - Layout.fillWidth: true - text: qsTr("Accounts") - font.bold: true - color: Theme.palette.text - } - } - - // Account list: tap a row to make it the active account, then - // return to the main view so the selection is reflected. - ListView { - Layout.fillWidth: true - Layout.preferredHeight: Math.min(contentHeight, 260) - clip: true - model: root.accountModel - spacing: Theme.spacing.small - ScrollIndicator.vertical: ScrollIndicator { } - - delegate: AccountDelegate { - width: ListView.view.width - highlighted: index === root.selectedIndex - onClicked: { - root.selectedIndex = index - viewStack.pop() - } - onCopyRequested: (text) => root.copyToClipboard(text) - } - } - - LogosButton { - Layout.fillWidth: true - height: 40 - text: qsTr("Add") - // Leave the wallet menu open behind the (modal) dialog. - onClicked: createAccountDialog.open() - } - } - } - - // ── Settings view: back + editable network (sequencer) ──────────── - Component { - id: settingsView - - ColumnLayout { - spacing: Theme.spacing.medium - - // Header: back to the main view + title. - RowLayout { - Layout.fillWidth: true - spacing: Theme.spacing.small - - WalletIconButton { - iconSource: Qt.resolvedUrl("internal/icons/back.svg") - onClicked: viewStack.pop() - } - LogosText { - Layout.fillWidth: true - text: qsTr("Settings") - font.bold: true - color: Theme.palette.text - } - } - - LogosText { - text: qsTr("Network (sequencer URL)") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - } - - LogosTextField { - id: seqField - Layout.fillWidth: true - placeholderText: "http://127.0.0.1:3040" - // Initialize from the live value without binding, so typing - // isn't clobbered when sequencerAddr updates after a save. - Component.onCompleted: text = root.backend ? root.backend.sequencerAddr : "" - } - - LogosText { - id: seqStatus - Layout.fillWidth: true - visible: text.length > 0 - wrapMode: Text.WordWrap - font.pixelSize: Theme.typography.secondaryText - property bool ok: false - color: ok ? Theme.palette.success : Theme.palette.error - } - - LogosButton { - Layout.fillWidth: true - height: 40 - text: qsTr("Save") - onClicked: { - if (!root.backend) return - seqStatus.text = "" - logos.watch(root.backend.changeSequencerAddr(seqField.text), - function(ok) { - seqStatus.ok = ok - seqStatus.text = ok ? qsTr("Network updated.") - : qsTr("Failed to update network.") - }, - function(error) { - seqStatus.ok = false - seqStatus.text = qsTr("Error: %1").arg(error) - }) - } - } - } - } - } - - // ── Dialogs ────────────────────────────────────────────────────────── - CreateWalletDialog { - id: createWalletDialog - walletHome: root.backend ? root.backend.walletHome : "" - onCreateWallet: function(password) { - if (!root.backend) return - // createNewDefault returns the new wallet's seed phrase (empty on - // failure). On success we hand it to the dialog, which switches to - // its backup page — we do NOT close here, so the user can't skip it. - logos.watch(root.backend.createNewDefault(password), - function(mnemonic) { - if (mnemonic && mnemonic.length > 0) - createWalletDialog.mnemonic = mnemonic - else - createWalletDialog.createError = qsTr("Failed to create wallet. Please try again.") - }, - function(error) { - createWalletDialog.createError = qsTr("Error creating wallet: %1").arg(error) - }) - } - onCopyRequested: function(text) { - if (root.backend) root.backend.copyToClipboard(text) - } - } - - CreateAccountDialog { - id: createAccountDialog - onCreatePublicRequested: { - if (!root.backend) return - logos.watch(root.backend.createAccountPublic(), - function(_id) { /* model updates via NOTIFY after refresh */ }, - function(error) { console.warn("createAccountPublic failed:", error) }) - } - onCreatePrivateRequested: { - if (!root.backend) return - logos.watch(root.backend.createAccountPrivate(), - function(_id) { /* model updates via NOTIFY after refresh */ }, - function(error) { console.warn("createAccountPrivate failed:", error) }) - } - } -} diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/AccountDelegate.qml b/apps/common/wallet-ui/qml/Logos/Wallet/internal/AccountDelegate.qml deleted file mode 100644 index 14c03ee..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/AccountDelegate.qml +++ /dev/null @@ -1,84 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import Logos.Theme -import Logos.Controls - -// One account row in the account dropdown. Ported from the LEZ wallet UI. -ItemDelegate { - id: root - - // Emitted when the user clicks the copy icon; the parent view connects this - // to its QML-side clipboard helper (AccountControl.copyToClipboard). - signal copyRequested(string text) - - leftPadding: Theme.spacing.medium - rightPadding: Theme.spacing.medium - topPadding: Theme.spacing.medium - bottomPadding: Theme.spacing.medium - - background: Rectangle { - color: root.highlighted || root.hovered ? - Theme.palette.backgroundMuted : - Theme.palette.backgroundTertiary - radius: Theme.spacing.radiusLarge - } - - contentItem: ColumnLayout { - spacing: Theme.spacing.small - RowLayout { - Layout.fillWidth: true - spacing: Theme.spacing.small - - LogosText { - text: model.name ?? "" - font.pixelSize: Theme.typography.secondaryText - font.bold: true - } - - Rectangle { - Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2 - Layout.preferredHeight: tagLabel.implicitHeight + 4 - radius: 4 - color: Theme.palette.backgroundSecondary - - LogosText { - id: tagLabel - anchors.centerIn: parent - text: model.isPublic ? qsTr("Public") : qsTr("Private") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - } - } - - Item { Layout.fillWidth: true } - - LogosText { - text: model.balance && model.balance.length > 0 ? model.balance : "—" - font.bold: true - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 0 - LogosText { - id: addressLabel - Layout.fillWidth: true - verticalAlignment: Text.AlignVCenter - text: model.address ?? "" - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textMuted - elide: Text.ElideMiddle - } - LogosCopyButton { - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - onCopyText: root.copyRequested(model.address) - visible: addressLabel.text - icon.color: Theme.palette.textMuted - } - } - } -} diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateAccountDialog.qml b/apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateAccountDialog.qml deleted file mode 100644 index 8070795..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateAccountDialog.qml +++ /dev/null @@ -1,102 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import Logos.Theme -import Logos.Controls - -// Public/private account creation dialog. Ported from the LEZ wallet UI. -Popup { - id: root - - signal createPublicRequested() - signal createPrivateRequested() - - modal: true - dim: true - padding: Theme.spacing.large - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - // Center on the full-window overlay rather than the small navbar control - // this popup is declared inside. - parent: Overlay.overlay - anchors.centerIn: parent - width: 360 - - background: Rectangle { - color: Theme.palette.backgroundSecondary - radius: Theme.spacing.radiusXlarge - border.color: Theme.palette.backgroundElevated - } - - contentItem: ColumnLayout { - id: contentLayout - // Pin to the popup's padded width so children stay within the modal. - width: root.availableWidth - spacing: Theme.spacing.large - - LogosText { - text: qsTr("Create account") - font.pixelSize: Theme.typography.titleText - font.weight: Theme.typography.weightBold - color: Theme.palette.text - } - - LogosText { - text: qsTr("Choose account type.") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - Layout.topMargin: -Theme.spacing.small - } - - RowLayout { - Layout.fillWidth: true - spacing: Theme.spacing.medium - - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - LogosText { - text: qsTr("Private") - font.pixelSize: Theme.typography.primaryText - color: Theme.palette.text - } - LogosText { - text: qsTr("Private balance and activity.") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - } - - LogosSwitch { - id: privateSwitch - checked: false - } - } - - RowLayout { - Layout.topMargin: Theme.spacing.medium - spacing: Theme.spacing.medium - Layout.fillWidth: true - LogosButton { - text: qsTr("Cancel") - Layout.fillWidth: true - onClicked: root.close() - } - LogosButton { - text: qsTr("Create") - Layout.fillWidth: true - onClicked: { - if (privateSwitch.checked) - root.createPrivateRequested() - else - root.createPublicRequested() - root.close() - } - } - } - } -} diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateWalletDialog.qml b/apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateWalletDialog.qml deleted file mode 100644 index 05c8d6e..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/CreateWalletDialog.qml +++ /dev/null @@ -1,201 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import Logos.Theme -import Logos.Controls - -// Wallet creation modal. Two pages, driven by whether a mnemonic exists yet: -// 1. Password entry — emits createWallet(password); the parent creates the -// wallet and, on success, sets `mnemonic` to the returned seed phrase. -// 2. Seed-phrase backup — shows the mnemonic once and gates dismissal behind -// an explicit acknowledgement. This is the only time the phrase is shown, -// so the popup is not auto-dismissable while it is visible. -// Storage/config live at the per-app default (backend.walletHome) — no path -// picking. Opened from the navbar "Connect" button. -Popup { - id: root - - // Where the wallet will be stored, shown for transparency. - property string walletHome: "" - property string createError: "" - // Set by the parent to the BIP39 seed phrase once creation succeeds. A - // non-empty value flips the dialog to the backup page. - property string mnemonic: "" - - signal createWallet(string password) - signal copyRequested(string text) - - modal: true - dim: true - padding: Theme.spacing.large - // Once the wallet exists we must not let the user dismiss the modal (and - // lose the only view of their seed phrase) by clicking away or pressing Esc. - closePolicy: root.mnemonic.length > 0 - ? Popup.NoAutoClose - : (Popup.CloseOnEscape | Popup.CloseOnPressOutside) - // Center on the full-window overlay rather than the small navbar control - // this popup is declared inside. - parent: Overlay.overlay - anchors.centerIn: parent - width: 380 - - onOpened: { - passwordField.text = "" - confirmField.text = "" - root.createError = "" - root.mnemonic = "" - passwordField.forceActiveFocus() - } - - background: Rectangle { - color: Theme.palette.backgroundSecondary - radius: Theme.spacing.radiusXlarge - border.color: Theme.palette.backgroundElevated - } - - contentItem: ColumnLayout { - // Pin to the popup's padded width so long text wraps and fillWidth - // children don't push the layout wider than the modal. - width: root.availableWidth - spacing: 0 - - // ── Page 1: password entry ──────────────────────────────────────── - ColumnLayout { - id: passwordPage - visible: root.mnemonic.length === 0 - Layout.fillWidth: true - spacing: Theme.spacing.large - - LogosText { - text: qsTr("Create your wallet") - font.pixelSize: Theme.typography.titleText - font.weight: Theme.typography.weightBold - color: Theme.palette.text - } - - LogosText { - text: qsTr("Secure your wallet with a password. It will be stored on this device at %1.") - .arg(root.walletHome || qsTr("the default location")) - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - Layout.topMargin: -Theme.spacing.small - } - - LogosTextField { - id: passwordField - Layout.fillWidth: true - placeholderText: qsTr("Password") - echoMode: TextInput.Password - Keys.onReturnPressed: createButton.tryCreate() - } - LogosTextField { - id: confirmField - Layout.fillWidth: true - placeholderText: qsTr("Confirm password") - echoMode: TextInput.Password - Keys.onReturnPressed: createButton.tryCreate() - } - - LogosText { - Layout.fillWidth: true - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.error - wrapMode: Text.WordWrap - visible: text.length > 0 - text: root.createError - } - - RowLayout { - Layout.topMargin: Theme.spacing.small - Layout.fillWidth: true - spacing: Theme.spacing.medium - LogosButton { - text: qsTr("Cancel") - Layout.fillWidth: true - onClicked: root.close() - } - LogosButton { - id: createButton - Layout.fillWidth: true - text: qsTr("Create Wallet") - function tryCreate() { - if (passwordField.text.length === 0) { - root.createError = qsTr("Password cannot be empty.") - } else if (passwordField.text !== confirmField.text) { - root.createError = qsTr("Passwords do not match.") - } else { - root.createError = "" - root.createWallet(passwordField.text) - } - } - onClicked: tryCreate() - } - } - } - - // ── Page 2: seed-phrase backup ──────────────────────────────────── - ColumnLayout { - id: backupPage - visible: root.mnemonic.length > 0 - Layout.fillWidth: true - spacing: Theme.spacing.large - - LogosText { - text: qsTr("Back up your recovery phrase") - font.pixelSize: Theme.typography.titleText - font.weight: Theme.typography.weightBold - color: Theme.palette.text - } - - LogosText { - text: qsTr("Write these words down in order and store them somewhere safe. Anyone with this phrase can control your wallet, and it will not be shown again — it is the only way to recover access.") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - wrapMode: Text.WordWrap - Layout.fillWidth: true - Layout.topMargin: -Theme.spacing.small - } - - Rectangle { - Layout.fillWidth: true - radius: Theme.spacing.radiusLarge - color: Theme.palette.backgroundElevated - implicitHeight: phraseText.implicitHeight + 2 * Theme.spacing.medium - - LogosText { - id: phraseText - anchors.fill: parent - anchors.margins: Theme.spacing.medium - text: root.mnemonic - wrapMode: Text.WordWrap - lineHeight: 1.4 - font.pixelSize: Theme.typography.primaryText - font.weight: Theme.typography.weightBold - color: Theme.palette.text - } - } - - LogosButton { - Layout.fillWidth: true - text: qsTr("Copy to clipboard") - onClicked: root.copyRequested(root.mnemonic) - } - - LogosCheckbox { - id: ackCheck - Layout.fillWidth: true - text: qsTr("I have safely backed up my recovery phrase") - } - - LogosButton { - Layout.fillWidth: true - enabled: ackCheck.checked - text: qsTr("Continue") - onClicked: root.close() - } - } - } -} diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/LogosCopyButton.qml b/apps/common/wallet-ui/qml/Logos/Wallet/internal/LogosCopyButton.qml deleted file mode 100644 index 7c1d7bd..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/LogosCopyButton.qml +++ /dev/null @@ -1,39 +0,0 @@ -import QtQuick -import QtQuick.Controls - -import Logos.Theme - -Button { - id: root - - signal copyText() - - implicitWidth: 24 - implicitHeight: 24 - display: AbstractButton.IconOnly - flat: true - - property string iconSource: Qt.resolvedUrl("icons/copy.svg") - - icon.source: root.iconSource - icon.width: 24 - icon.height: 24 - icon.color: Theme.palette.textSecondary - - function reset() { - iconSource = Qt.resolvedUrl("icons/copy.svg") - } - - Timer { - id: resetTimer - interval: 1500 - repeat: false - onTriggered: root.reset() - } - - onClicked: { - root.copyText() - root.iconSource = Qt.resolvedUrl("icons/checkmark.svg") - resetTimer.restart() - } -} diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/WalletIconButton.qml b/apps/common/wallet-ui/qml/Logos/Wallet/internal/WalletIconButton.qml deleted file mode 100644 index 61545e0..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/WalletIconButton.qml +++ /dev/null @@ -1,25 +0,0 @@ -import QtQuick -import QtQuick.Controls - -import Logos.Theme - -// Small icon-only action button for the wallet menu. Uses the same Button + -// icon.source/icon.color path as LogosCopyButton, which renders reliably here -// (LogosIconButton's Image + MultiEffect shader path does not). -Button { - id: root - - property url iconSource - property color iconColor: Theme.palette.textSecondary - property int iconSize: 18 - - implicitWidth: 32 - implicitHeight: 32 - display: AbstractButton.IconOnly - flat: true - - icon.source: root.iconSource - icon.width: root.iconSize - icon.height: root.iconSize - icon.color: root.iconColor -} diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/account.svg b/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/account.svg deleted file mode 100644 index 779d76a..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/account.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/back.svg b/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/back.svg deleted file mode 100644 index 6960875..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/back.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/checkmark.svg b/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/checkmark.svg deleted file mode 100644 index 2eabb54..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/checkmark.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/copy.svg b/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/copy.svg deleted file mode 100644 index cfa6cf8..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/copy.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/power.svg b/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/power.svg deleted file mode 100644 index 5900ca3..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/power.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/settings.svg b/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/settings.svg deleted file mode 100644 index 77f89ae..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/internal/icons/settings.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/common/wallet-ui/qml/Logos/Wallet/qmldir b/apps/common/wallet-ui/qml/Logos/Wallet/qmldir deleted file mode 100644 index 53183e8..0000000 --- a/apps/common/wallet-ui/qml/Logos/Wallet/qmldir +++ /dev/null @@ -1,4 +0,0 @@ -module Logos.Wallet -# Public API. Implementation controls live under internal/ and are intentionally -# not exported — importers of Logos.Wallet only see WalletControl. -WalletControl 1.0 WalletControl.qml diff --git a/apps/common/wallet-ui/src/AccountModel.cpp b/apps/common/wallet-ui/src/AccountModel.cpp deleted file mode 100644 index 63992f8..0000000 --- a/apps/common/wallet-ui/src/AccountModel.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "AccountModel.h" - -#include - -AccountModel::AccountModel(QObject* parent) - : QAbstractListModel(parent) -{ -} - -int AccountModel::rowCount(const QModelIndex& parent) const -{ - if (parent.isValid()) - return 0; - return m_entries.size(); -} - -QVariant AccountModel::data(const QModelIndex& index, int role) const -{ - if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size()) - return QVariant(); - - const AccountEntry& e = m_entries.at(index.row()); - switch (role) { - case NameRole: return e.name; - case AddressRole: return e.address; - case BalanceRole: return e.balance; - case IsPublicRole: return e.isPublic; - default: return QVariant(); - } -} - -QHash AccountModel::roleNames() const -{ - return { - { NameRole, "name" }, - { AddressRole, "address" }, - { BalanceRole, "balance" }, - { IsPublicRole, "isPublic" } - }; -} - -void AccountModel::replaceFromJsonArray(const QJsonArray& arr) -{ - beginResetModel(); - const int oldCount = m_entries.size(); - m_entries.clear(); - int idx = 0; - for (const QJsonValue& v : arr) { - AccountEntry e; - e.name = QStringLiteral("Account %1").arg(++idx); - e.balance = QString(); - if (v.isObject()) { - const QJsonObject obj = v.toObject(); - e.address = obj.value(QStringLiteral("account_id")).toString(); - e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true); - } else { - e.address = v.toString(); - e.isPublic = true; - } - m_entries.append(e); - } - endResetModel(); - if (oldCount != m_entries.size()) - emit countChanged(); -} - -void AccountModel::setBalanceByAddress(const QString& address, const QString& balance) -{ - for (int i = 0; i < m_entries.size(); ++i) { - if (m_entries.at(i).address == address) { - if (m_entries.at(i).balance != balance) { - m_entries[i].balance = balance; - const QModelIndex idx = index(i, 0); - emit dataChanged(idx, idx, { BalanceRole }); - } - return; - } - } -} diff --git a/apps/common/wallet-ui/src/AccountModel.h b/apps/common/wallet-ui/src/AccountModel.h deleted file mode 100644 index 5509b15..0000000 --- a/apps/common/wallet-ui/src/AccountModel.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -// One wallet account row. Mirrors the shape returned by the core wallet -// module's list_accounts() (account_id + is_public), with a display name and -// a lazily-fetched balance. -struct AccountEntry { - QString name; - QString address; - QString balance; - bool isPublic = true; -}; - -// QAbstractListModel of wallet accounts, exposed to QML via -// logos.model("_ui", "accountModel"). Shared across LEZ apps -// (apps/common/wallet-ui); ported from the LEZ wallet UI. -class AccountModel : 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 AccountModel(QObject* parent = nullptr); - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; - - void replaceFromJsonArray(const QJsonArray& arr); - void setBalanceByAddress(const QString& address, const QString& balance); - int count() const { return m_entries.size(); } - -signals: - void countChanged(); - -private: - QVector m_entries; -}; diff --git a/apps/common/wallet-ui/src/WalletBackendLogic.h b/apps/common/wallet-ui/src/WalletBackendLogic.h deleted file mode 100644 index b3bf9ce..0000000 --- a/apps/common/wallet-ui/src/WalletBackendLogic.h +++ /dev/null @@ -1,453 +0,0 @@ -#ifndef WALLET_BACKEND_LOGIC_H -#define WALLET_BACKEND_LOGIC_H - -// Shared wallet backend logic for LEZ ui_qml apps (apps/common/wallet-ui). -// -// Every app's backend derives from a per-app QtRO SimpleSource generated from -// its own UiBackend.rep. Those .rep files are byte-identical except for -// the class name, so the generated SimpleSources expose an identical property -// surface (isWalletOpen, walletExists, configPath, storagePath, walletHome, -// lastSyncedBlock, currentBlockHeight, sequencerAddr, sequencerReachable) and -// the same wallet slots. That lets the whole implementation live here once, -// parameterised on the generated SimpleSource: WalletBackendLogic derives -// from Base, so it can reach Base's protected PROP setters and override Base's -// pure-virtual .rep slots directly. -// -// A host backend is then a near-empty shell — it only adds Q_OBJECT, the -// accountModel Q_PROPERTY, and a constructor that names the module: -// -// class FooUiBackend : public WalletBackendLogic { -// Q_OBJECT -// Q_PROPERTY(AccountModel* accountModel READ accountModel CONSTANT) -// public: -// explicit FooUiBackend(LogosAPI* api = nullptr, QObject* parent = nullptr) -// : WalletBackendLogic(api, parent, "foo_ui", "FooUI") {} -// }; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "logos_api.h" -#include "logos_sdk.h" - -#include "AccountModel.h" - -// Base is the generated UiBackendSimpleSource. We inherit it so we can -// access its protected PROP setters and override its pure-virtual .rep slots. -template -class WalletBackendLogic : public Base { - static constexpr const char* SETTINGS_ORG = "Logos"; - // Sticky "user pressed Disconnect" flag so the wallet stays locked across - // relaunches until the user reconnects. - static constexpr const char* DISCONNECTED_KEY = "disconnected"; - static constexpr int WALLET_FFI_SUCCESS = 0; - // Wallet home env override. Mirrors LEZ's own var so the app shares the - // canonical wallet (~/.lee/wallet) used by the wallet UI and other apps. - static constexpr const char* WALLET_HOME_ENV = "LEE_WALLET_HOME_DIR"; - - // Normalise file:// URLs and OS paths to a plain local path. - static QString toLocalPath(const QString& path) { - if (path.startsWith("file://") || path.contains("/")) - return QUrl::fromUserInput(path).toLocalFile(); - return path; - } - -public: - // moduleName: the LEZ module name (e.g. "token_ui"), used for the fallback - // LogosAPI. settingsApp: the QSettings application key (e.g. "TokenUI"). - WalletBackendLogic(LogosAPI* logosAPI, QObject* parent, - const char* moduleName, const char* settingsApp) - : Base(parent), - m_settingsApp(settingsApp), - m_accountModel(new AccountModel(this)), - m_logosAPI(logosAPI ? logosAPI : new LogosAPI(moduleName, this)), - m_logos(new LogosModules(m_logosAPI)), - m_net(new QNetworkAccessManager(this)), - m_reachabilityTimer(new QTimer(this)) - { - // PROP defaults via the generated (protected) setters. - this->setIsWalletOpen(false); - this->setLastSyncedBlock(0); - this->setCurrentBlockHeight(0); - this->setWalletHome(defaultWalletHome()); - // Assume reachable until a probe proves otherwise (avoids a startup flash). - this->setSequencerReachable(true); - - // Periodically re-probe the sequencer so the banner reacts to a node - // going up/down while the app is running. Probes are no-ops until a - // wallet (and thus a sequencer address) is open. - m_reachabilityTimer->setInterval(10000); - QObject::connect(m_reachabilityTimer, &QTimer::timeout, this, - [this]() { checkReachability(); }); - m_reachabilityTimer->start(); - - // Always resolve against the canonical wallet home (LEE_WALLET_HOME_DIR - // or ~/.lee/wallet). We intentionally don't seed config/storage paths - // from QSettings: a previously-persisted per-app path would otherwise - // override the default and pin the app to the old keystore. - - // A wallet exists on disk if its storage file is present (drives whether - // the navbar "Connect" reconnects or offers to create a wallet). - const QString effectiveStorage = - this->storagePath().isEmpty() ? defaultStoragePath() : this->storagePath(); - this->setWalletExists(QFileInfo::exists(effectiveStorage)); - - // ui-host runs our constructor inside initLogos(), synchronously, BEFORE - // it enables remoting and emits READY. Any blocking RPC here would stall - // ui-host startup past its ready watchdog. Defer the open+refresh chain - // to the first event-loop tick so ui-host finishes wiring itself up. - QTimer::singleShot(0, this, [this]() { openOrAdoptWallet(); }); - - // Save wallet on quit; host may not call destructors so this is - // best-effort. - QObject::connect(qApp, &QCoreApplication::aboutToQuit, this, - [this]() { saveWallet(); }, Qt::DirectConnection); - } - - ~WalletBackendLogic() override - { - saveWallet(); - delete m_logos; - } - - AccountModel* accountModel() const { return m_accountModel; } - - // ── .rep slot overrides ────────────────────────────────────────────────── - - QString createAccountPublic() override - { - const QString result = m_logos->logos_execution_zone.create_account_public(); - if (!result.isEmpty()) - refreshAccounts(); - return result; - } - - QString createAccountPrivate() override - { - const QString result = m_logos->logos_execution_zone.create_account_private(); - if (!result.isEmpty()) - refreshAccounts(); - return result; - } - - void refreshAccounts() override - { - const QJsonArray arr = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()); - m_accountModel->replaceFromJsonArray(arr); - refreshBalances(); - } - - void refreshBalances() override - { - refreshBlockHeights(); - if (this->currentBlockHeight() > 0) - m_logos->logos_execution_zone.sync_to_block(static_cast(this->currentBlockHeight())); - - for (int i = 0; i < m_accountModel->count(); ++i) { - const QModelIndex idx = m_accountModel->index(i, 0); - const QString addr = m_accountModel->data(idx, AccountModel::AddressRole).toString(); - const bool isPub = m_accountModel->data(idx, AccountModel::IsPublicRole).toBool(); - m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub)); - } - saveWallet(); - } - - QString getBalance(QString accountIdHex, bool isPublic) override - { - return m_logos->logos_execution_zone.get_balance(accountIdHex, isPublic); - } - - QString createNewDefault(QString password) override - { - QDir().mkpath(defaultWalletHome()); - return createNew(defaultConfigPath(), defaultStoragePath(), password); - } - - QString createNew(QString configPath, QString storagePath, QString password) override - { - const QString localConfig = toLocalPath(configPath); - const QString localStorage = toLocalPath(storagePath); - // create_new returns the new wallet's BIP39 mnemonic (empty on failure). - // We hand it back to the caller instead of discarding it: wallet creation - // is the only moment the seed phrase is recoverable, so the UI must force - // a backup step before the user can proceed. - const QString mnemonic = m_logos->logos_execution_zone.create_new(localConfig, localStorage, password); - if (mnemonic.isEmpty()) { - qWarning() << m_settingsApp << "backend: create_new failed (empty mnemonic)"; - return QString(); - } - - persistConfigPath(localConfig); - persistStoragePath(localStorage); - this->setWalletExists(true); - QSettings(SETTINGS_ORG, m_settingsApp).setValue(DISCONNECTED_KEY, false); - this->setIsWalletOpen(true); - refreshAccounts(); - refreshBlockHeights(); - refreshSequencerAddr(); - return mnemonic; - } - - bool openExisting() override - { - // Adopt a shared open wallet (Basecamp), else open our own from disk. A - // freshly-created shared wallet can be open with zero accounts, so probe - // open-ness rather than keying off list_accounts() alone. - if (sharedWalletIsOpen()) { - const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()); - this->setIsWalletOpen(true); - m_accountModel->replaceFromJsonArray(existing); - refreshBalances(); - refreshSequencerAddr(); - QSettings(SETTINGS_ORG, m_settingsApp).setValue(DISCONNECTED_KEY, false); - return true; - } - - const QString cfg = this->configPath().isEmpty() ? defaultConfigPath() : this->configPath(); - const QString stg = this->storagePath().isEmpty() ? defaultStoragePath() : this->storagePath(); - if (!QFileInfo::exists(stg)) - return false; - - const int err = m_logos->logos_execution_zone.open(cfg, stg); - if (err != WALLET_FFI_SUCCESS) { - qWarning() << m_settingsApp << "backend: openExisting failed, code" << err; - return false; - } - persistConfigPath(cfg); - persistStoragePath(stg); - this->setIsWalletOpen(true); - QSettings(SETTINGS_ORG, m_settingsApp).setValue(DISCONNECTED_KEY, false); - refreshAccounts(); - refreshBlockHeights(); - refreshSequencerAddr(); - return true; - } - - void disconnectWallet() override - { - // UI-local lock: persist wallet state, drop our view of it, and remember - // the choice. We do NOT close the core module's wallet handle — in - // Basecamp that instance is shared with other apps. - saveWallet(); - this->setIsWalletOpen(false); - m_accountModel->replaceFromJsonArray(QJsonArray()); - QSettings(SETTINGS_ORG, m_settingsApp).setValue(DISCONNECTED_KEY, true); - } - - bool changeSequencerAddr(QString url) override - { - const QString trimmed = url.trimmed(); - if (trimmed.isEmpty()) { - qWarning() << m_settingsApp << "backend: refusing to set empty sequencer_addr"; - return false; - } - - const QString cfg = this->configPath().isEmpty() ? defaultConfigPath() : this->configPath(); - - // Preserve the other config fields (poll timeouts, retries) — only swap - // the endpoint. The wallet reads this file on open via - // from_path_or_initialize_default. - QJsonObject obj; - QFile in(cfg); - if (in.open(QIODevice::ReadOnly)) { - obj = QJsonDocument::fromJson(in.readAll()).object(); - in.close(); - } - obj.insert(QStringLiteral("sequencer_addr"), trimmed); - - QFile out(cfg); - if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - qWarning() << m_settingsApp << "backend: cannot write wallet config" << cfg; - return false; - } - out.write(QJsonDocument(obj).toJson(QJsonDocument::Indented)); - out.close(); - - // Re-open so the live wallet uses the new endpoint right away. - if (this->isWalletOpen()) { - const QString stg = this->storagePath().isEmpty() ? defaultStoragePath() : this->storagePath(); - const int err = m_logos->logos_execution_zone.open(cfg, stg); - if (err != WALLET_FFI_SUCCESS) { - qWarning() << m_settingsApp << "backend: reopen after sequencer change failed, code" << err; - return false; - } - refreshSequencerAddr(); - refreshAccounts(); - } - return true; - } - - void copyToClipboard(QString text) override - { - if (QGuiApplication::clipboard()) - QGuiApplication::clipboard()->setText(text); - } - -private: - // ── Internal helpers (not part of the .rep slot surface) ───────────────── - - static QString defaultWalletHome() - { - const QByteArray override = qgetenv(WALLET_HOME_ENV); - if (!override.isEmpty()) - return QString::fromLocal8Bit(override); - // LEZ's canonical wallet home, shared with the wallet UI and other LEZ - // apps (matches lez/wallet get_home_default_path()). - return QDir::homePath() + QStringLiteral("/.lee/wallet"); - } - - QString defaultConfigPath() const - { - return defaultWalletHome() + QStringLiteral("/wallet_config.json"); - } - - QString defaultStoragePath() const - { - return defaultWalletHome() + QStringLiteral("/storage.json"); - } - - void openOrAdoptWallet() - { - // Respect an explicit user disconnect: stay locked, show "Connect". - if (QSettings(SETTINGS_ORG, m_settingsApp).value(DISCONNECTED_KEY, false).toBool()) - return; - - // In Basecamp the logos_execution_zone module is a single shared - // instance, so the wallet may already be open (e.g. opened by the - // dedicated wallet app). Adopt that wallet instead of fighting over it: - // mirror its state rather than re-opening from disk, which could clobber - // unsaved in-memory accounts the other app holds. A freshly-created - // shared wallet can be open with zero accounts, so we can't key off - // list_accounts() alone (see sharedWalletIsOpen). - if (sharedWalletIsOpen()) { - const QJsonArray existing = QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()); - qDebug() << m_settingsApp << "backend: adopting already-open shared wallet" - << existing.size() << "accounts"; - this->setIsWalletOpen(true); - m_accountModel->replaceFromJsonArray(existing); - refreshBalances(); - refreshSequencerAddr(); - return; - } - - // Standalone (own core instance): auto-open a previously-created wallet. - // Use persisted paths if the user picked custom ones, else the per-app - // default. Only open if the storage actually exists, otherwise stay - // closed so QML shows the "Connect" entry point (no noisy FFI errors on - // first run). - const QString cfg = this->configPath().isEmpty() ? defaultConfigPath() : this->configPath(); - const QString stg = this->storagePath().isEmpty() ? defaultStoragePath() : this->storagePath(); - if (!QFileInfo::exists(stg)) - return; // No wallet yet — QML shows "Connect". - - qDebug() << m_settingsApp << "backend: opening wallet with config" << cfg << "storage" << stg; - const int err = m_logos->logos_execution_zone.open(cfg, stg); - if (err == WALLET_FFI_SUCCESS) { - persistConfigPath(cfg); - persistStoragePath(stg); - this->setIsWalletOpen(true); - refreshAccounts(); - refreshBlockHeights(); - refreshSequencerAddr(); - } else { - qWarning() << m_settingsApp << "backend: wallet open failed, code" << err; - } - } - - bool sharedWalletIsOpen() - { - // list_accounts() is non-empty only once the wallet holds accounts, so - // it can't distinguish "no wallet open" from "open but empty" (a wallet - // that was just created and hasn't had an account added yet). Fall back - // to a handle-dependent, account-independent signal: an open wallet - // always has a sequencer address (from its config, defaulted on open), - // while a closed core returns an empty string. This lets us adopt a - // freshly-created shared wallet instead of re-opening it from disk. - if (!QJsonArray::fromVariantList(m_logos->logos_execution_zone.list_accounts()).isEmpty()) - return true; - return !m_logos->logos_execution_zone.get_sequencer_addr().isEmpty(); - } - - void refreshBlockHeights() - { - const int lastVal = m_logos->logos_execution_zone.get_last_synced_block(); - const int currentVal = m_logos->logos_execution_zone.get_current_block_height(); - if (this->lastSyncedBlock() != lastVal) - this->setLastSyncedBlock(lastVal); - if (this->currentBlockHeight() != currentVal) - this->setCurrentBlockHeight(currentVal); - } - - void refreshSequencerAddr() - { - const QString addr = m_logos->logos_execution_zone.get_sequencer_addr(); - if (this->sequencerAddr() != addr) - this->setSequencerAddr(addr); - // Probe right away so the banner reflects the (possibly new) endpoint - // without waiting for the next periodic tick. - checkReachability(); - } - - void checkReachability() - { - const QString addr = this->sequencerAddr(); - if (addr.isEmpty()) - return; - - QNetworkRequest req{QUrl(addr)}; - req.setTransferTimeout(4000); - QNetworkReply* reply = m_net->get(req); - QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { - // Any HTTP response (even a 404) means the node is up; only a - // transport failure (connection refused, host not found, timeout) - // counts as down. - const bool gotHttpStatus = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid(); - const bool reachable = gotHttpStatus || reply->error() == QNetworkReply::NoError; - if (this->sequencerReachable() != reachable) - this->setSequencerReachable(reachable); - reply->deleteLater(); - }); - } - - void saveWallet() - { - if (this->isWalletOpen()) - m_logos->logos_execution_zone.save(); - } - - // These only update the in-session PROPs (so subsequent open/refresh calls - // reuse the same path). They are not written to QSettings: the app always - // resolves against the canonical wallet home, so there's nothing to remember - // across launches. - void persistConfigPath(const QString& path) { this->setConfigPath(toLocalPath(path)); } - void persistStoragePath(const QString& path) { this->setStoragePath(toLocalPath(path)); } - - QString m_settingsApp; - - AccountModel* m_accountModel; - - LogosAPI* m_logosAPI; - LogosModules* m_logos; - - QNetworkAccessManager* m_net; - QTimer* m_reachabilityTimer; -}; - -#endif // WALLET_BACKEND_LOGIC_H diff --git a/apps/token/CMakeLists.txt b/apps/token/CMakeLists.txt index 6488bdf..17c7635 100644 --- a/apps/token/CMakeLists.txt +++ b/apps/token/CMakeLists.txt @@ -1,14 +1,31 @@ -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.21) project(TokenUiPlugin LANGUAGES CXX) +find_package(Qt6 6.8 REQUIRED COMPONENTS Core Gui Network Qml Quick QuickControls2) +qt_standard_project_setup(REQUIRES 6.8) + +include(CTest) + if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) else() message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") endif() -# ui_qml module with a hand-written C++ backend (QtRO .rep view contract + -# generated *SimpleSource/*ViewPluginBase). Mirrors the LEZ wallet UI module. +# The wallet access library is shared by Basecamp UI modules. The generated +# Logos SDK directory is supplied by logos-module-builder at configure time. +set(LOGOS_WALLET_SOURCE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/../shared/wallet" + CACHE PATH "Path to the shared Logos wallet module" +) +set(LOGOS_WALLET_GENERATED_DIR + "${CMAKE_CURRENT_BINARY_DIR}/generated_code" + CACHE PATH "Path to generated Logos SDK sources" +) +add_subdirectory("${LOGOS_WALLET_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/shared-wallet") + +# UI module with a hand-written C++ adapter. The adapter forwards token +# operations to token_module and owns only wallet-session/UI aggregation logic. logos_module( NAME token_ui REP_FILE src/TokenUiBackend.rep @@ -18,14 +35,10 @@ logos_module( src/TokenUiPlugin.cpp src/TokenUiBackend.h src/TokenUiBackend.cpp - # Shared wallet UI, overlaid from apps/common/wallet-ui at build time. - src/WalletBackendLogic.h - src/AccountModel.h - src/AccountModel.cpp FIND_PACKAGES Qt6Gui - Qt6Network LINK_LIBRARIES Qt6::Gui - Qt6::Network + LINK_TARGETS + logos_wallet_access ) diff --git a/apps/token/README.md b/apps/token/README.md index 12c8df2..a68c122 100644 --- a/apps/token/README.md +++ b/apps/token/README.md @@ -1,19 +1,18 @@ # Token UI -A QML UI application for the token program. +A Basecamp-compatible QML UI module for creating and inspecting Token Program +assets through the `token_module` core module. See the [Logos QML UI App Tutorial](https://github.com/logos-co/logos-tutorial/blob/master/tutorial-qml-ui-app.md) for more information. -> **Status:** interactive UI prototype. The **Create** view models the current -> Token Program's fungible and non-fungible definition settings, and **Inspect** -> renders the supplied July 12, 2026 testnet fixture snapshot. It never creates -> accounts, signs, reads a live chain, or submits a transaction. +The UI creates fresh public wallet accounts, submits fungible and +non-fungible definition transactions through `token_module`, and reads the +connected wallet's Token-owned accounts for the Inspect view. Raw `u128` +values stay decimal strings across the QML/C++ boundary. -## Token-definition prototype +## Token definitions -The prototype is deliberately local-only. It lets an operator prepare every -currently supported creation shape and see the resulting stored state before a -production transaction path exists: +The Create view supports every current definition shape: - Fungible definitions: raw `u128` supply; fixed (`None`), self, or external mint authority; metadata omitted or linked. @@ -26,25 +25,24 @@ production transaction path exists: - Target accounts: definition, first holding/master, and metadata (when used) are surfaced because each must be fresh and authorized at submission time. -The **Inspect** fixtures are a historical testnet reference, not a live read. -They include fixed, self-authorized, external-authority, revoked-authority, and -metadata-backed fungibles, plus the Glitchlings NFT collection. Display decimals -shown for fungibles are UI inference from the fixture policy; the Token Program -does not store a decimal field. +Before a wallet is connected, Inspect shows bundled example records so the +module remains useful as a visual shell. After connection it replaces those +records with live `walletTokenAccounts`, `inspectDefinition`, and +`inspectMetadata` results. Display decimals shown for bundled examples are UI +inference; the Token Program does not store a decimal field. -Metadata-backed definitions and NFT definitions need typed instruction -serialization today because the generic IDL cannot encode the structured -creation arguments. Any production submission implementation must use that -route rather than treating this prototype as a transaction client. +Metadata-backed and NFT definitions use the typed token-module submission path; +the UI does not assemble transaction instructions itself. ## Wallet / chain integration This app is a `ui_qml` module with a hand-written C++ backend (`src/TokenUiBackend.*`, plugin in `src/TokenUiPlugin.*`) that depends on the -core **`logos_execution_zone`** wallet module. The backend calls the core -module's wallet FFI through `m_logos->logos_execution_zone.*` and exposes an -async QtRO surface (`src/TokenUiBackend.rep`) plus an account list model to the -QML view. The wallet backend and navbar are ported from the AMM UI app. +core **`logos_execution_zone`** wallet module and **`token_module`** Token +Program API. The backend exposes an async QtRO surface +(`src/TokenUiBackend.rep`) plus an account list model to the QML view. Wallet +session behavior and the `Logos.Wallet` control come from +`apps/shared/wallet`. **Onboarding is non-invasive.** The app opens straight to the first screen; the navbar shows **Connect** (opens a password-only modal) or **Connected** + the @@ -58,8 +56,8 @@ Account/keystore sharing follows the runtime: `~/.lee/wallet` keystore is shared with the LEZ wallet UI and any other LEZ app on the machine. A previously-created wallet auto-opens on launch. - **Inside Basecamp**: the core wallet module is a single shared instance, so on - startup the backend **adopts** the already-open wallet (see - `openOrAdoptWallet()`), surfacing **shared** accounts across apps. + startup `LogosWalletProvider` adopts the already-open wallet, surfacing + **shared** accounts across apps. ## Setup @@ -71,10 +69,10 @@ mkdir -p ~/.config/nix && echo "experimental-features = nix-command flakes" >> ~ ## Running the UI -Start the UI with: +From the repository root, start the packaged UI with: ```bash -nix run . +nix run .#token-ui ``` This builds and runs the application in development mode. diff --git a/apps/token/flake.lock b/apps/token/flake.lock index 00ee11b..727d6af 100644 --- a/apps/token/flake.lock +++ b/apps/token/flake.lock @@ -26768,7 +26768,7 @@ "logos-module-builder", "nixpkgs" ], - "wallet_ui": "wallet_ui" + "shared_wallet": "shared_wallet" } }, "rust-overlay": { @@ -26855,14 +26855,14 @@ "type": "github" } }, - "wallet_ui": { + "shared_wallet": { "flake": false, "locked": { - "path": "../common/wallet-ui", + "path": "../shared/wallet", "type": "path" }, "original": { - "path": "../common/wallet-ui", + "path": "../shared/wallet", "type": "path" }, "parent": [] diff --git a/apps/token/flake.nix b/apps/token/flake.nix index 8fdd345..870cc69 100644 --- a/apps/token/flake.nix +++ b/apps/token/flake.nix @@ -1,71 +1,38 @@ { - description = "Logos Token QML UI — create and manage tokens on the LEZ token program"; + description = "Logos Token QML UI — create and inspect Token Program assets"; inputs = { logos-module-builder.url = "github:logos-co/logos-module-builder"; - - # Core wallet module (the LEZ wallet FFI Qt plugin). The input name must - # match the metadata.json `dependencies` entry so the builder can resolve - # it as a module dependency. This rev pins LEZ (lssa) at fb8cbac4, which - # includes the macOS Metal-build fix, so no `--override-input` is needed. - logos_execution_zone.url = "github:logos-blockchain/logos-execution-zone-module?rev=d2e9400ac06c3cdbfc2405b4f153fff9841a453c"; - - # Shared wallet UI, consumed as a plain source tree — not a flake: - # - src/{AccountModel,WalletBackendLogic} — C++ overlaid onto this app's - # source below (the backend is compiled into this app's plugin). - # - qml/Logos/Wallet — the importable `Logos.Wallet` QML module, installed - # into the app output at lib/Logos/Wallet by postInstall (the standalone - # puts the app's plugin dir on the QML import path, so `import - # Logos.Wallet` resolves at runtime). - wallet_ui = { - url = "path:../common/wallet-ui"; + shared_wallet = { + url = "path:../shared/wallet"; flake = false; }; - - # Build the merge with the exact nixpkgs the module builder uses, so there is - # no second nixpkgs to download and no version skew. - nixpkgs.follows = "logos-module-builder/nixpkgs"; }; - outputs = inputs@{ self, logos-module-builder, wallet_ui, nixpkgs, ... }: - let - systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]; - forAllSystems = f: nixpkgs.lib.genAttrs systems f; - - # Overlay the shared wallet UI onto this app's own source. Built per system - # so cross-platform builds stay pure (the module builder takes a single - # `src`, so we produce a system-matched merged tree for each). - mergedSrcFor = system: - let pkgs = import nixpkgs { inherit system; }; - in pkgs.runCommand "token-ui-src" { } '' - cp -r ${self}/. $out - chmod -R u+w $out - mkdir -p $out/src $out/qml - # Shared C++ (account model + CRTP wallet backend logic). - cp ${wallet_ui}/src/AccountModel.h $out/src/AccountModel.h - cp ${wallet_ui}/src/AccountModel.cpp $out/src/AccountModel.cpp - cp ${wallet_ui}/src/WalletBackendLogic.h $out/src/WalletBackendLogic.h - # Shared Logos.Wallet QML module, placed under the view dir so it ships - # at /qml/Logos/Wallet and NavBar can `import "Logos/Wallet"`. - # (The ui-host only searches the runtime's own QML import path, not the - # app dir, so a bare `import Logos.Wallet` URI would not resolve; a - # relative import against the view dir does. See the shared README.) - rm -rf $out/qml/Logos - cp -r ${wallet_ui}/qml/Logos $out/qml/Logos - ''; - - # Call the builder once per system with that system's merged src, then keep - # only that system's outputs. (mkLogosQmlModule iterates all systems - # internally; feeding each call the matching source keeps the diagonal - # correct and everything off it lazily unevaluated.) - moduleFor = system: logos-module-builder.lib.mkLogosQmlModule { - src = mergedSrcFor system; - configFile = ./metadata.json; - flakeInputs = inputs; - }; - in { - packages = forAllSystems (system: (moduleFor system).packages.${system}); - apps = forAllSystems (system: (moduleFor system).apps.${system}); - devShells = forAllSystems (system: (moduleFor system).devShells.${system}); + # The repository root is the supported build for this UI because it injects + # the in-tree token_module core module. Keep this file useful for local QML + # iteration and consistent with the AMM UI's standalone source layout. + outputs = inputs@{ logos-module-builder, shared_wallet, ... }: + logos-module-builder.lib.mkLogosQmlModule { + src = ./.; + configFile = ./metadata.json; + flakeInputs = inputs; + preConfigure = '' + cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${shared_wallet}") + cmakeFlagsArray+=("-DLOGOS_WALLET_GENERATED_DIR=$PWD/generated_code/include") + ''; + externalLibInputs = { }; + postInstall = '' + walletQmlDescriptor="$(find "$PWD" -type f -path '*/shared-wallet/qml/Logos/Wallet/qmldir' -print -quit)" + if [ -z "$walletQmlDescriptor" ]; then + echo "Built Logos.Wallet QML module not found" + exit 1 + fi + walletQmlDir="$(dirname "$walletQmlDescriptor")" + walletQmlInstallDir="$out/lib/Logos/Wallet" + mkdir -p "$walletQmlInstallDir" + cp -r "$walletQmlDir/." "$walletQmlInstallDir/" + test -f "$walletQmlInstallDir/qmldir" + ''; }; } diff --git a/apps/token/metadata.json b/apps/token/metadata.json index 95d06f4..ef618c6 100644 --- a/apps/token/metadata.json +++ b/apps/token/metadata.json @@ -3,11 +3,11 @@ "version": "0.1.0", "type": "ui_qml", "category": "token", - "description": "UI module for the token program", + "description": "Logos UI module for creating and inspecting Token Program assets", "main": "token_ui_plugin", "view": "qml/Main.qml", "icon": "icons/token.png", - "dependencies": ["logos_execution_zone"], + "dependencies": ["logos_execution_zone", "token_module"], "nix": { "packages": { diff --git a/apps/token/qml/Logos/Wallet/qmldir b/apps/token/qml/Logos/Wallet/qmldir new file mode 100644 index 0000000..866351b --- /dev/null +++ b/apps/token/qml/Logos/Wallet/qmldir @@ -0,0 +1,5 @@ +module Logos.Wallet +optional plugin logos_wallet_qmlplugin ../../../Logos/Wallet +classname Logos_WalletPlugin +prefer :/qt/qml/Logos/Wallet/ +depends QtQuick diff --git a/apps/token/qml/Main.qml b/apps/token/qml/Main.qml index 1288997..e3011a8 100644 --- a/apps/token/qml/Main.qml +++ b/apps/token/qml/Main.qml @@ -14,10 +14,10 @@ Item { property bool ready: false - // Local-only prototype data. It is seeded from the supplied testnet - // deployment record and never performs a chain read or submits a call. - TokenPrototypeStore { - id: tokenPrototypeStore + // Fixture data remains available before a wallet is connected. Once the + // wallet opens, ManagePage replaces it with live token-module reads. + TokenStore { + id: tokenStore } Connections { @@ -67,8 +67,8 @@ Item { } } - // The app is always usable; the wallet is opt-in via the navbar "Connect" - // control. Prototype views render immediately and stay local-only. + // The app is usable before wallet connection; writes and live reads become + // available as soon as the navbar opens a wallet. NavBar { id: navbar anchors.top: connectionBanner.bottom @@ -89,13 +89,19 @@ Item { CreatePage { anchors.fill: parent visible: navbar.currentIndex === 0 - store: tokenPrototypeStore + store: tokenStore + backend: root.ready ? root.backend : null + runtime: logos + + onRequestInspect: navbar.currentIndex = 1 } ManagePage { anchors.fill: parent visible: navbar.currentIndex === 1 - store: tokenPrototypeStore + store: tokenStore + backend: root.ready ? root.backend : null + runtime: logos } } } diff --git a/apps/token/qml/NavBar.qml b/apps/token/qml/NavBar.qml index 4af1a22..7cd4427 100644 --- a/apps/token/qml/NavBar.qml +++ b/apps/token/qml/NavBar.qml @@ -4,12 +4,7 @@ import QtQuick 2.15 import QtQuick.Layouts 1.15 import Logos.Theme - -// Shared wallet UI module (apps/common/wallet-ui). Imported by relative path -// because the ui-host only searches the runtime's own QML import path, not the -// app's plugin dir. Once the module ships as a compiled qrc module (see PR #228) -// this becomes `import Logos.Wallet`. -import "Logos/Wallet" +import Logos.Wallet // Self-contained navigation bar — styling is independent of any view's theme. // Use currentIndex to read the active tab; tabChanged(index) fires on selection. @@ -22,6 +17,7 @@ Item { // Wallet wiring, passed down from Main.qml. property var backend: null property var accountModel: null + readonly property bool compact: width < 560 // Address of the account currently selected in the header control. readonly property string selectedAddress: accountControl.selectedAddress @@ -45,9 +41,9 @@ Item { RowLayout { anchors.fill: parent - anchors.leftMargin: 20 - anchors.rightMargin: 20 - spacing: 4 + anchors.leftMargin: root.compact ? 12 : 20 + anchors.rightMargin: root.compact ? 12 : 20 + spacing: root.compact ? 2 : 4 // App identity Text { @@ -77,7 +73,7 @@ Item { readonly property bool active: root.currentIndex === tabIndex height: 36 - width: tabLabel.implicitWidth + 28 + width: tabLabel.implicitWidth + (root.compact ? 20 : 28) radius: 18 color: active ? Theme.palette.backgroundSecondary : "transparent" border.color: activeFocus ? Theme.palette.overlayOrange : "transparent" @@ -128,9 +124,14 @@ Item { // Wallet / account control on the far right. WalletControl { id: accountControl - Layout.leftMargin: 12 - backend: root.backend + Layout.leftMargin: root.compact ? 4 : 12 + compact: root.compact + wallet: root.backend accountModel: root.accountModel + viewportWidth: root.width + watchCall: function(result, success, failure) { + logos.watch(result, success, failure) + } } } } diff --git a/apps/token/qml/pages/CreatePage.qml b/apps/token/qml/pages/CreatePage.qml index d5cf5f0..f244485 100644 --- a/apps/token/qml/pages/CreatePage.qml +++ b/apps/token/qml/pages/CreatePage.qml @@ -1,46 +1,57 @@ -/* - * THESIS: A definition workbench makes the token program's few irreversible - * choices legible before a user ever signs. It refuses a generic dashboard in - * favor of one live creation sheet, one outcome preview, and one readiness rail. - * OWN-WORLD: Existing LEZ charcoal surfaces, warm text, thin borders, and amber - * only for selection and the single primary action. - * STORY: An operator chooses fungible or NFT, exposes every protocol setting, - * sees exact resulting state, then prepares—not submits—a definition draft. - * FIRST VIEWPORT: Input sheet left, state preview center, signer/readiness rail - * right; narrow windows stack those regions in reading order. - * FORM: Grounded surface candidate 3, seed eda5e259. FINISH: unreviewed and - * undocumented is unfinished; this build ends with the finish review, the - * verdict, and DESIGN.md. - */ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls.Basic +import QtQuick.Layouts Item { id: root property var store: null + property var backend: null + property var runtime: null property int tokenKind: 0 property int authorityMode: 0 property bool metadataEnabled: false property int metadataStandard: 0 + property int step: 0 property bool prepared: false property string preparedMessage: "" + property string errorMessage: "" + property bool submitting: false + property bool accountBusy: false + + signal requestInspect() + + onVisibleChanged: { + if (visible) { + step = 0; + prepared = false; + preparedMessage = ""; + errorMessage = ""; + submitting = false; + scroll.contentY = 0; + } + } readonly property bool isFungible: tokenKind === 0 readonly property bool requiresMetadata: !isFungible readonly property bool hasMetadata: requiresMetadata || metadataEnabled readonly property string maximumU128: "340282366920938463463374607431768211455" - readonly property string instructionName: isFungible && !hasMetadata ? "new_fungible_definition" : "new_definition_with_metadata" + readonly property string instructionName: !isFungible ? "createNonFungible" : hasMetadata ? "createFungibleWithMetadata" : "createFungible" readonly property string supplyLabel: isFungible ? qsTr("Initial raw supply") : qsTr("Printable supply") readonly property string supplyValue: supplyField.text.length > 0 ? supplyField.text : qsTr("Not set") - readonly property string authorityValue: !isFungible ? qsTr("Not applicable to NFT definitions") : authorityMode === 0 ? qsTr("None — fixed supply") : authorityMode === 1 ? qsTr("Definition account — self authority") : externalAuthorityField.text.length > 0 ? externalAuthorityField.text : qsTr("External authority required") + readonly property string authorityValue: !isFungible ? qsTr("Master holding controls printing") : authorityMode === 0 ? qsTr("Fixed supply") : authorityMode === 1 ? qsTr("Definition account") : externalAuthorityField.text.length > 0 ? externalAuthorityField.text : qsTr("External account required") readonly property bool validSupply: isUnsignedU128(supplyField.text) readonly property bool validDefinitionTarget: isAccountId(definitionTargetField.text) readonly property bool validHoldingTarget: isAccountId(holdingTargetField.text) readonly property bool validMetadataTarget: !hasMetadata || isAccountId(metadataTargetField.text) readonly property bool validExternalAuthority: !isFungible || authorityMode !== 2 || (isAccountId(externalAuthorityField.text) && externalAuthorityField.text !== "11111111111111111111111111111111") - readonly property bool canPrepare: validSupply && validDefinitionTarget && validHoldingTarget && validMetadataTarget && validExternalAuthority + readonly property bool canContinue: validSupply && validExternalAuthority + readonly property bool canPrepare: canContinue && validDefinitionTarget && validHoldingTarget && validMetadataTarget + readonly property bool canSubmit: canPrepare && root.backend !== null && root.backend.isWalletOpen && !root.submitting + readonly property int validTargetCount: (validDefinitionTarget ? 1 : 0) + (validHoldingTarget ? 1 : 0) + (hasMetadata && validMetadataTarget ? 1 : 0) + readonly property int targetCount: hasMetadata ? 3 : 2 function isUnsignedU128(value) { if (!/^[0-9]+$/.test(value)) @@ -55,18 +66,85 @@ Item { } function isAccountId(value) { - return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value); + return /^[0-9a-fA-F]{64}$/.test(value) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value); + } + + function clearDraftState() { + prepared = false; + preparedMessage = ""; + errorMessage = ""; + } + + function watch(result, success, failure) { + if (root.runtime && root.runtime.watch) + root.runtime.watch(result, success, failure); + else if (failure) + failure(qsTr("Runtime is not ready.")); + } + + function createTargetAccounts() { + if (!root.backend || !root.backend.isWalletOpen || root.accountBusy) + return; + + var fields = [definitionTargetField, holdingTargetField]; + if (root.hasMetadata) + fields.push(metadataTargetField); + + var pendingFields = []; + for (var index = 0; index < fields.length; ++index) { + if (!fields[index].text) + pendingFields.push(fields[index]); + } + + if (pendingFields.length === 0) + return; + + root.accountBusy = true; + function createNext(fieldIndex) { + if (fieldIndex >= pendingFields.length) { + root.accountBusy = false; + root.clearDraftState(); + return; + } + root.watch(root.backend.createAccountPublic(), function(accountId) { + if (!accountId) { + root.accountBusy = false; + root.errorMessage = qsTr("Could not create a public wallet account."); + return; + } + pendingFields[fieldIndex].text = String(accountId); + createNext(fieldIndex + 1); + }, function(error) { + root.accountBusy = false; + root.errorMessage = qsTr("Could not create a public wallet account: %1").arg(error); + }); + } + createNext(0); + } + + function selectTokenKind(kind) { + tokenKind = kind; + if (requiresMetadata) + metadataEnabled = true; + clearDraftState(); } function setPattern(pattern) { - prepared = false; - preparedMessage = ""; + clearDraftState(); + step = 0; + externalAuthorityField.text = ""; + definitionTargetField.text = ""; + holdingTargetField.text = ""; + metadataTargetField.text = ""; + metadataUriField.text = ""; + creatorsField.text = ""; if (pattern === "fixed") { tokenKind = 0; authorityMode = 0; metadataEnabled = false; - nameField.text = qsTr("Example fixed supply"); + metadataStandard = 0; + nameField.text = qsTr("Fixed supply token"); supplyField.text = "7654321"; return; } @@ -76,7 +154,7 @@ Item { authorityMode = 2; metadataEnabled = true; metadataStandard = 1; - nameField.text = qsTr("Example metadata token"); + nameField.text = qsTr("Metadata token"); supplyField.text = "10000000000000000000000000"; metadataUriField.text = "data:application/json;base64,..."; creatorsField.text = qsTr("Creator or authority label"); @@ -84,45 +162,135 @@ Item { } tokenKind = 1; + authorityMode = 0; metadataEnabled = true; metadataStandard = 1; - nameField.text = qsTr("Example collection"); + nameField.text = qsTr("NFT collection"); supplyField.text = "64"; metadataUriField.text = "data:application/json;base64,..."; creatorsField.text = qsTr("Master-holding creator"); } function prepareDefinition() { - if (!canPrepare) + if (!canSubmit) return; - var draft = { - id: "draft-" + Date.now(), - name: nameField.text.length > 0 ? nameField.text : qsTr("Untitled definition"), - type: isFungible ? "fungible" : "nonFungible", - definitionId: definitionTargetField.text, - holdingId: holdingTargetField.text, - metadataId: hasMetadata ? metadataTargetField.text : "", - rawSupply: supplyField.text, - displaySupply: supplyField.text, - inferredDecimals: "", - authorityMode: isFungible ? (authorityMode === 0 ? "fixed" : authorityMode === 1 ? "self" : "external") : "masterHolding", - authority: isFungible && authorityMode === 2 ? externalAuthorityField.text : authorityMode === 1 ? definitionTargetField.text : "", - metadataStandard: hasMetadata ? (metadataStandard === 0 ? "Simple" : "Expanded") : "", - metadataUri: hasMetadata ? metadataUriField.text : "", - creators: hasMetadata ? creatorsField.text : "", - description: qsTr("Prepared locally. No token account or transaction was created."), - source: "draft", - instruction: instructionName, - printableCopies: !isFungible ? supplyField.text : "", - masterHolding: !isFungible ? holdingTargetField.text : "" - }; + var mintAuthority = !isFungible ? "" : authorityMode === 0 ? "none" : authorityMode === 1 ? "self" : externalAuthorityField.text; + var metadataStandardValue = metadataStandard === 0 ? "simple" : "expanded"; + var pending; + root.submitting = true; + root.errorMessage = ""; + root.preparedMessage = qsTr("Submitting to the Token Program…"); - if (store && store.addDraft) - store.addDraft(draft); + if (!isFungible && root.backend) { + pending = root.backend.createNonFungible( + definitionTargetField.text, + holdingTargetField.text, + metadataTargetField.text, + nameField.text, + supplyField.text, + metadataStandardValue, + metadataUriField.text, + creatorsField.text); + } else if (hasMetadata && root.backend) { + pending = root.backend.createFungibleWithMetadata( + definitionTargetField.text, + holdingTargetField.text, + metadataTargetField.text, + nameField.text, + supplyField.text, + mintAuthority, + metadataStandardValue, + metadataUriField.text, + creatorsField.text); + } else if (root.backend) { + pending = root.backend.createFungible( + definitionTargetField.text, + holdingTargetField.text, + nameField.text, + supplyField.text, + mintAuthority); + } - prepared = true; - preparedMessage = qsTr("Draft prepared locally. Switch to Inspect to review it alongside the testnet fixtures."); + root.watch(pending, function(result) { + root.submitting = false; + if (!result || result.status !== "ok") { + root.prepared = false; + root.errorMessage = qsTr("Token Program rejected the request: %1").arg(result && result.error ? result.error : qsTr("unknown_error")); + return; + } + + var transactionId = String(result.transactionId || ""); + var definitionType = isFungible ? "fungible" : "nonFungible"; + var draft = { + id: definitionTargetField.text, + name: nameField.text.length > 0 ? nameField.text : qsTr("Untitled definition"), + type: definitionType, + definitionId: definitionTargetField.text, + holdingId: holdingTargetField.text, + metadataId: hasMetadata ? metadataTargetField.text : "", + rawSupply: supplyField.text, + displaySupply: supplyField.text, + inferredDecimals: "", + authorityMode: isFungible ? (authorityMode === 0 ? "fixed" : authorityMode === 1 ? "self" : "external") : "masterHolding", + authority: isFungible && authorityMode === 2 ? externalAuthorityField.text : authorityMode === 1 ? definitionTargetField.text : "", + authorityLabel: isFungible && authorityMode === 2 ? qsTr("External authority account") : "", + metadataStandard: hasMetadata ? (metadataStandard === 0 ? "Simple" : "Expanded") : "", + metadataUri: hasMetadata ? metadataUriField.text : "", + creators: hasMetadata ? creatorsField.text : "", + description: qsTr("Submitted to the Token Program."), + source: "pending", + instruction: instructionName, + printableCopies: !isFungible ? supplyField.text : "", + masterHolding: !isFungible ? holdingTargetField.text : "", + transactionId: transactionId, + definition: { + id: definitionTargetField.text, + hex: definitionTargetField.text, + name: nameField.text, + type: definitionType, + totalSupplyRaw: isFungible ? supplyField.text : undefined, + printableSupply: !isFungible ? supplyField.text : undefined, + mintAuthority: isFungible && authorityMode === 2 ? externalAuthorityField.text : isFungible && authorityMode === 1 ? definitionTargetField.text : undefined, + metadataId: hasMetadata ? metadataTargetField.text : undefined + }, + holding: { + id: holdingTargetField.text, + wallet: "connected wallet", + role: !isFungible ? "nftMaster" : "fungible", + rawBalance: isFungible ? supplyField.text : undefined, + printBalance: !isFungible ? supplyField.text : undefined + }, + holdings: [{ + id: holdingTargetField.text, + wallet: "connected wallet", + role: !isFungible ? "nftMaster" : "fungible", + rawBalance: isFungible ? supplyField.text : undefined, + printBalance: !isFungible ? supplyField.text : undefined + }] + }; + + if (hasMetadata) { + draft.metadata = { + id: metadataTargetField.text, + standard: metadataStandard === 0 ? "Simple" : "Expanded", + uri: metadataUriField.text, + creators: creatorsField.text + }; + } + + if (store && store.addDraft) + store.addDraft(draft); + + root.prepared = true; + root.preparedMessage = transactionId.length > 0 + ? qsTr("Transaction submitted · %1").arg(transactionId) + : qsTr("Transaction submitted."); + }, function(error) { + root.submitting = false; + root.prepared = false; + root.errorMessage = qsTr("Token Program request failed: %1").arg(error); + }); } Rectangle { @@ -130,198 +298,319 @@ Item { color: "#151515" } + Menu { + id: examplesMenu + + MenuItem { + text: qsTr("Fixed supply") + onTriggered: root.setPattern("fixed") + } + + MenuItem { + text: qsTr("Metadata-backed fungible") + onTriggered: root.setPattern("metadata") + } + + MenuItem { + text: qsTr("NFT collection") + onTriggered: root.setPattern("nft") + } + } + Flickable { id: scroll anchors.fill: parent clip: true contentWidth: width - contentHeight: content.implicitHeight + 48 + contentHeight: contentColumn.implicitHeight + 32 flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { + id: verticalScrollBar + + parent: scroll.parent + anchors.top: scroll.top + anchors.right: scroll.right + anchors.bottom: scroll.bottom + anchors.topMargin: 4 + anchors.rightMargin: 4 + anchors.bottomMargin: 4 + policy: scroll.contentHeight > scroll.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff + active: true + visible: policy === ScrollBar.AlwaysOn + width: 12 + z: 10 + + background: Rectangle { + radius: 6 + color: "#292929" + } + + contentItem: Rectangle { + implicitWidth: 8 + implicitHeight: 32 + radius: 4 + color: verticalScrollBar.pressed ? "#F26A21" : "#9A8C81" + opacity: 1 + } + } + ColumnLayout { - id: content + id: contentColumn - width: Math.max(280, Math.min(scroll.width - 40, 1440)) - x: Math.max(20, (scroll.width - width) / 2) - y: 24 - spacing: 18 + width: Math.max(240, Math.min(scroll.width - 32, 1120)) + x: Math.max(16, (scroll.width - width) / 2) + y: 16 + spacing: 12 + + ColumnLayout { + id: pageHeader - RowLayout { Layout.fillWidth: true - spacing: 18 + spacing: 8 ColumnLayout { Layout.fillWidth: true - spacing: 5 + Layout.minimumWidth: 0 + spacing: 3 Text { + Layout.fillWidth: true + Layout.minimumWidth: 0 color: "#E7E1D8" font.pixelSize: 28 font.weight: Font.DemiBold - text: qsTr("Create token definition") + wrapMode: Text.Wrap + text: qsTr("Create definition") } Text { Layout.fillWidth: true + Layout.minimumWidth: 0 color: "#A9A098" font.pixelSize: 14 wrapMode: Text.Wrap - text: qsTr("Prepare the exact definition shape first. This prototype does not create accounts, sign, or submit a transaction.") + text: qsTr("Configure the token, confirm its account targets, then create the definition.") } } - Rectangle { - Layout.alignment: Qt.AlignTop - Layout.preferredHeight: 28 - Layout.preferredWidth: prototypeLabel.implicitWidth + 18 - radius: 14 - color: "#211914" - border.color: "#49301F" - border.width: 1 + RowLayout { + id: progressRow - Text { - id: prototypeLabel + Layout.fillWidth: true + Layout.minimumWidth: 0 + spacing: 5 - anchors.centerIn: parent - color: "#F2D8C7" - font.pixelSize: 12 - font.weight: Font.DemiBold - text: qsTr("Prototype") + Repeater { + model: [qsTr("Configure"), qsTr("Account targets"), qsTr("Review")] + + delegate: RowLayout { + id: stepItem + + required property int index + required property var modelData + + Layout.fillWidth: true + Layout.minimumWidth: 0 + spacing: 5 + + Rectangle { + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + radius: 12 + color: root.step === stepItem.index ? "#F26A21" : root.step > stepItem.index ? "#183222" : "#202020" + border.color: root.step === stepItem.index ? "#F26A21" : root.step > stepItem.index ? "#39C06A" : "#343434" + border.width: 1 + + Text { + anchors.centerIn: parent + color: root.step === stepItem.index ? "#151515" : root.step > stepItem.index ? "#78C88D" : "#A9A098" + font.pixelSize: 12 + font.weight: Font.DemiBold + text: (stepItem.index + 1).toString() + } + } + + Text { + Layout.fillWidth: true + Layout.minimumWidth: 0 + color: root.step === stepItem.index ? "#E7E1D8" : "#A9A098" + font.pixelSize: 13 + font.weight: root.step === stepItem.index ? Font.DemiBold : Font.Normal + horizontalAlignment: Text.AlignLeft + wrapMode: Text.Wrap + text: stepItem.modelData + verticalAlignment: Text.AlignVCenter + } + + Rectangle { + Layout.fillWidth: true + Layout.minimumWidth: 0 + Layout.preferredHeight: 1 + visible: stepItem.index < 2 + color: root.step > stepItem.index ? "#39C06A" : "#343434" + } + } } } } GridLayout { - id: workbench + id: stage - readonly property int columnCount: content.width >= 1220 ? 3 : content.width >= 820 ? 2 : 1 + readonly property int columnCount: contentColumn.width >= 860 ? 2 : 1 Layout.fillWidth: true + Layout.minimumWidth: 0 columns: columnCount - columnSpacing: 14 - rowSpacing: 14 + columnSpacing: 12 + rowSpacing: 12 Rectangle { - id: formPanel + id: mainPanel Layout.alignment: Qt.AlignTop Layout.fillWidth: true - Layout.preferredWidth: workbench.columnCount === 3 ? 520 : 0 - Layout.columnSpan: workbench.columnCount === 1 ? 1 : 1 - implicitHeight: formContent.implicitHeight + 32 + Layout.minimumWidth: 0 + Layout.preferredWidth: stage.columnCount === 2 ? 640 : 0 + Layout.maximumWidth: stage.columnCount === 2 ? 680 : 1120 + implicitHeight: editor.implicitHeight + 32 radius: 16 color: "#1B1B1B" border.color: "#303030" border.width: 1 ColumnLayout { - id: formContent + id: editor anchors.fill: parent - anchors.margins: 16 - spacing: 14 + anchors.margins: 14 + spacing: 10 - Text { - color: "#E7E1D8" - font.pixelSize: 18 - font.weight: Font.DemiBold - text: qsTr("Definition settings") + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + + Text { + color: "#E7E1D8" + font.pixelSize: 20 + font.weight: Font.DemiBold + text: root.step === 0 ? qsTr("Token settings") : root.step === 1 ? qsTr("Account targets") : qsTr("Review definition") + } + + Text { + Layout.fillWidth: true + color: "#A9A098" + font.pixelSize: 13 + wrapMode: Text.Wrap + text: root.step === 0 ? qsTr("Choose the fields that define this token.") : root.step === 1 ? qsTr("These accounts are created with the token definition.") : qsTr("Review the definition before creating it.") + } } - TabBar { - id: kindTabs + ColumnLayout { + id: configureView Layout.fillWidth: true - Layout.preferredHeight: 42 - currentIndex: root.tokenKind + Layout.preferredHeight: visible ? implicitHeight : 0 + spacing: 10 + visible: root.step === 0 - background: Rectangle { + Rectangle { + id: kindTabs + + Layout.fillWidth: true + Layout.preferredHeight: 44 + Layout.minimumHeight: 44 + Layout.maximumHeight: 44 radius: 8 color: "#101010" border.color: "#343434" border.width: 1 - } - onCurrentIndexChanged: { - root.tokenKind = currentIndex; - if (root.requiresMetadata) - root.metadataEnabled = true; - root.prepared = false; - } + RowLayout { + anchors.fill: parent + spacing: 0 - TabButton { - id: fungibleTab + Button { + id: fungibleTab - text: qsTr("Fungible") - activeFocusOnTab: true - Accessible.name: qsTr("Create fungible token definition") + property bool selected: root.tokenKind === 0 - contentItem: Text { - color: fungibleTab.checked ? "#F2D8C7" : "#A9A098" - font.pixelSize: 14 - font.weight: fungibleTab.checked ? Font.DemiBold : Font.Normal - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: fungibleTab.text - } + Layout.fillWidth: true + Layout.fillHeight: true + text: qsTr("Fungible") + activeFocusOnTab: true + Accessible.name: qsTr("Create fungible token definition") + onClicked: root.selectTokenKind(0) - background: Rectangle { - radius: 7 - color: fungibleTab.checked ? "#211914" : "transparent" - border.color: fungibleTab.activeFocus ? "#FFB26B" : fungibleTab.checked ? "#F26A21" : "transparent" - border.width: fungibleTab.activeFocus ? 2 : 1 + contentItem: Text { + color: fungibleTab.selected ? "#F2D8C7" : "#A9A098" + font.pixelSize: 14 + font.weight: fungibleTab.selected ? Font.DemiBold : Font.Normal + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: fungibleTab.text + } + + background: Rectangle { + radius: 7 + color: fungibleTab.selected ? "#211914" : "transparent" + border.color: fungibleTab.activeFocus ? "#FFB26B" : fungibleTab.selected ? "#F26A21" : "transparent" + border.width: fungibleTab.activeFocus ? 2 : fungibleTab.selected ? 1 : 0 + } + } + + Button { + id: nonFungibleTab + + property bool selected: root.tokenKind === 1 + + Layout.fillWidth: true + Layout.fillHeight: true + text: qsTr("Non-fungible") + activeFocusOnTab: true + Accessible.name: qsTr("Create non-fungible token definition") + onClicked: root.selectTokenKind(1) + + contentItem: Text { + color: nonFungibleTab.selected ? "#F2D8C7" : "#A9A098" + font.pixelSize: 14 + font.weight: nonFungibleTab.selected ? Font.DemiBold : Font.Normal + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: nonFungibleTab.text + } + + background: Rectangle { + radius: 7 + color: nonFungibleTab.selected ? "#211914" : "transparent" + border.color: nonFungibleTab.activeFocus ? "#FFB26B" : nonFungibleTab.selected ? "#F26A21" : "transparent" + border.width: nonFungibleTab.activeFocus ? 2 : nonFungibleTab.selected ? 1 : 0 + } + } } } - TabButton { - id: nonFungibleTab - - text: qsTr("Non-fungible") - activeFocusOnTab: true - Accessible.name: qsTr("Create non-fungible token definition") - - contentItem: Text { - color: nonFungibleTab.checked ? "#F2D8C7" : "#A9A098" - font.pixelSize: 14 - font.weight: nonFungibleTab.checked ? Font.DemiBold : Font.Normal - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: nonFungibleTab.text - } - - background: Rectangle { - radius: 7 - color: nonFungibleTab.checked ? "#211914" : "transparent" - border.color: nonFungibleTab.activeFocus ? "#FFB26B" : nonFungibleTab.checked ? "#F26A21" : "transparent" - border.width: nonFungibleTab.activeFocus ? 2 : 1 - } + Text { + Layout.fillWidth: true + color: "#A9A098" + font.pixelSize: 13 + wrapMode: Text.Wrap + text: root.isFungible ? qsTr("One initial holding receives the full raw supply.") : qsTr("Metadata is required. The initial master holding controls printing.") } - } - Text { - Layout.fillWidth: true - color: "#A9A098" - font.pixelSize: 13 - wrapMode: Text.Wrap - text: root.isFungible ? qsTr("Fungible definitions create one initial holding with the full raw supply.") : qsTr("NFT definitions always include metadata and create a master holding that controls printing.") - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: "#303030" - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 7 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: "#303030" + } Text { color: "#E7E1D8" - font.pixelSize: 15 + font.pixelSize: 16 font.weight: Font.DemiBold - text: qsTr("Identity and supply") + text: qsTr("Identity") } Text { @@ -334,7 +623,7 @@ Item { id: nameField Layout.fillWidth: true - Layout.preferredHeight: 42 + Layout.preferredHeight: 44 Accessible.name: qsTr("Token definition name") color: "#E7E1D8" font.pixelSize: 15 @@ -343,10 +632,10 @@ Item { selectByMouse: true text: qsTr("New token") - onTextEdited: root.prepared = false + onTextEdited: root.clearDraftState() background: Rectangle { - radius: 6 + radius: 7 color: "#101010" border.color: nameField.activeFocus ? "#F26A21" : "#343434" border.width: 1 @@ -363,7 +652,7 @@ Item { id: supplyField Layout.fillWidth: true - Layout.preferredHeight: 42 + Layout.preferredHeight: 44 Accessible.name: root.supplyLabel color: "#E7E1D8" font.pixelSize: 15 @@ -376,10 +665,10 @@ Item { regularExpression: /^[0-9]*$/ } - onTextEdited: root.prepared = false + onTextEdited: root.clearDraftState() background: Rectangle { - radius: 6 + radius: 7 color: "#101010" border.color: supplyField.activeFocus ? "#F26A21" : supplyField.text.length > 0 && !root.validSupply ? "#D85F4B" : "#343434" border.width: 1 @@ -391,408 +680,399 @@ Item { color: root.validSupply ? "#A9A098" : "#F08A76" font.pixelSize: 12 wrapMode: Text.Wrap - text: root.validSupply ? qsTr("Raw u128 value. Decimal display precision is not stored by the Token Program.") : qsTr("Enter an unsigned 128-bit integer (0 through 340282366920938463463374607431768211455).") - } - } - - Rectangle { - Layout.fillWidth: true - color: "#303030" - visible: root.isFungible - Layout.preferredHeight: visible ? 1 : 0 - } - - ColumnLayout { - Layout.fillWidth: true - visible: root.isFungible - Layout.preferredHeight: visible ? implicitHeight : 0 - spacing: 7 - - Text { - color: "#E7E1D8" - font.pixelSize: 15 - font.weight: Font.DemiBold - text: qsTr("Mint authority") + text: root.validSupply ? qsTr("Raw u128 value. Display decimals are inferred by clients, not stored here.") : qsTr("Enter an unsigned 128-bit integer between 0 and 340282366920938463463374607431768211455.") } - Text { + ColumnLayout { Layout.fillWidth: true - color: "#A9A098" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: qsTr("This is the full authority surface at creation. NFTs have no definition-level mint authority.") - } + visible: root.isFungible + Layout.preferredHeight: visible ? implicitHeight : 0 + spacing: 8 - RadioButton { - id: fixedAuthority - - Layout.fillWidth: true - activeFocusOnTab: true - Accessible.name: qsTr("Fixed supply with no mint authority") - checked: root.authorityMode === 0 - text: qsTr("Fixed supply — no future minting") - onClicked: { - root.authorityMode = 0; - root.prepared = false; + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: "#303030" } - contentItem: Text { - leftPadding: fixedAuthority.indicator.width + fixedAuthority.spacing + Text { + color: "#E7E1D8" + font.pixelSize: 16 + font.weight: Font.DemiBold + text: qsTr("Mint authority") + } + + ComboBox { + id: authoritySelector + + Layout.fillWidth: true + Layout.preferredHeight: 44 + Accessible.name: qsTr("Mint authority policy") + currentIndex: root.authorityMode + model: [qsTr("Fixed supply — no future minting"), qsTr("Self authority — definition account mints"), qsTr("External authority — another account mints")] + + onActivated: { + root.authorityMode = currentIndex; + root.clearDraftState(); + } + + contentItem: Text { + leftPadding: 12 + rightPadding: authoritySelector.indicator.width + authoritySelector.spacing + color: "#E7E1D8" + elide: Text.ElideRight + font.pixelSize: 14 + text: authoritySelector.currentText + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + radius: 7 + color: "#101010" + border.color: authoritySelector.activeFocus ? "#F26A21" : "#343434" + border.width: 1 + } + } + + TextField { + id: externalAuthorityField + + Layout.fillWidth: true + Layout.preferredHeight: root.authorityMode === 2 ? 44 : 0 + Accessible.name: qsTr("External mint authority account ID") + clip: true color: "#E7E1D8" font.pixelSize: 14 - text: fixedAuthority.text - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - } + placeholderText: qsTr("External authority account ID") + placeholderTextColor: "#8E8780" + selectByMouse: true + visible: root.authorityMode === 2 - indicator: Rectangle { - implicitWidth: 18 - implicitHeight: 18 - x: fixedAuthority.leftPadding - y: fixedAuthority.topPadding + (fixedAuthority.availableHeight - height) / 2 - radius: 9 - color: "#101010" - border.color: fixedAuthority.activeFocus ? "#FFB26B" : fixedAuthority.checked ? "#F26A21" : "#8E8780" - border.width: fixedAuthority.activeFocus ? 2 : 1 + onTextEdited: root.clearDraftState() - Rectangle { - anchors.centerIn: parent - width: 8 - height: 8 - radius: 4 - color: "#F26A21" - visible: fixedAuthority.checked + background: Rectangle { + radius: 7 + color: "#101010" + border.color: externalAuthorityField.activeFocus ? "#F26A21" : externalAuthorityField.text.length > 0 && !root.validExternalAuthority ? "#D85F4B" : "#343434" + border.width: 1 } } } - RadioButton { - id: selfAuthority - + Rectangle { Layout.fillWidth: true - activeFocusOnTab: true - Accessible.name: qsTr("Use definition account as mint authority") - checked: root.authorityMode === 1 - text: qsTr("Self authority — definition account signs future mints") - onClicked: { - root.authorityMode = 1; - root.prepared = false; - } + Layout.preferredHeight: 1 + color: "#303030" + } - contentItem: Text { - leftPadding: selfAuthority.indicator.width + selfAuthority.spacing - color: "#E7E1D8" - font.pixelSize: 14 - text: selfAuthority.text - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - } + ColumnLayout { + Layout.fillWidth: true + spacing: 8 - indicator: Rectangle { - implicitWidth: 18 - implicitHeight: 18 - x: selfAuthority.leftPadding - y: selfAuthority.topPadding + (selfAuthority.availableHeight - height) / 2 - radius: 9 - color: "#101010" - border.color: selfAuthority.activeFocus ? "#FFB26B" : selfAuthority.checked ? "#F26A21" : "#8E8780" - border.width: selfAuthority.activeFocus ? 2 : 1 + RowLayout { + Layout.fillWidth: true + spacing: 10 - Rectangle { - anchors.centerIn: parent - width: 8 - height: 8 - radius: 4 - color: "#F26A21" - visible: selfAuthority.checked + ColumnLayout { + Layout.fillWidth: true + spacing: 3 + + Text { + color: "#E7E1D8" + font.pixelSize: 16 + font.weight: Font.DemiBold + text: qsTr("Metadata") + } + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: root.requiresMetadata ? qsTr("Required for non-fungible definitions") : qsTr("Optional for fungible definitions") + } + } + + CheckBox { + id: metadataCheckBox + + Layout.alignment: Qt.AlignVCenter + Accessible.name: qsTr("Include metadata account") + checked: root.hasMetadata + enabled: !root.requiresMetadata + text: qsTr("Include") + + onToggled: { + root.metadataEnabled = checked; + root.clearDraftState(); + } + + contentItem: Text { + leftPadding: metadataCheckBox.indicator.width + metadataCheckBox.spacing + color: metadataCheckBox.enabled ? "#B8ADA3" : "#6D6761" + font.pixelSize: 12 + text: metadataCheckBox.text + verticalAlignment: Text.AlignVCenter + } + + indicator: Rectangle { + implicitWidth: 18 + implicitHeight: 18 + x: metadataCheckBox.leftPadding + y: metadataCheckBox.topPadding + (metadataCheckBox.availableHeight - height) / 2 + radius: 4 + color: metadataCheckBox.checked ? "#F26A21" : "#101010" + border.color: metadataCheckBox.activeFocus ? "#FFB26B" : metadataCheckBox.checked ? "#F26A21" : "#8E8780" + border.width: metadataCheckBox.activeFocus ? 2 : 1 + + Rectangle { + anchors.centerIn: parent + width: 6 + height: 6 + radius: 3 + color: "#151515" + visible: metadataCheckBox.checked + } + } } } - } - - RadioButton { - id: externalAuthority - - Layout.fillWidth: true - activeFocusOnTab: true - Accessible.name: qsTr("Use external mint authority") - checked: root.authorityMode === 2 - text: qsTr("External authority — a separate account signs future mints") - onClicked: { - root.authorityMode = 2; - root.prepared = false; - } - - contentItem: Text { - leftPadding: externalAuthority.indicator.width + externalAuthority.spacing - color: "#E7E1D8" - font.pixelSize: 14 - text: externalAuthority.text - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - } - - indicator: Rectangle { - implicitWidth: 18 - implicitHeight: 18 - x: externalAuthority.leftPadding - y: externalAuthority.topPadding + (externalAuthority.availableHeight - height) / 2 - radius: 9 - color: "#101010" - border.color: externalAuthority.activeFocus ? "#FFB26B" : externalAuthority.checked ? "#F26A21" : "#8E8780" - border.width: externalAuthority.activeFocus ? 2 : 1 - - Rectangle { - anchors.centerIn: parent - width: 8 - height: 8 - radius: 4 - color: "#F26A21" - visible: externalAuthority.checked - } - } - } - - TextField { - id: externalAuthorityField - - Layout.fillWidth: true - Layout.preferredHeight: root.authorityMode === 2 ? 42 : 0 - visible: root.authorityMode === 2 - Accessible.name: qsTr("External mint authority account ID") - clip: true - color: "#E7E1D8" - font.pixelSize: 14 - placeholderText: qsTr("External authority account ID") - placeholderTextColor: "#8E8780" - selectByMouse: true - - onTextEdited: root.prepared = false - - background: Rectangle { - radius: 6 - color: "#101010" - border.color: externalAuthorityField.activeFocus ? "#F26A21" : externalAuthorityField.text.length > 0 && !root.validExternalAuthority ? "#D85F4B" : "#343434" - border.width: 1 - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: "#303030" - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 8 - - RowLayout { - Layout.fillWidth: true - spacing: 10 ColumnLayout { Layout.fillWidth: true - spacing: 3 + visible: root.hasMetadata + Layout.preferredHeight: visible ? implicitHeight : 0 + spacing: 8 + + Text { + Layout.fillWidth: true + color: "#A9A098" + font.pixelSize: 12 + wrapMode: Text.Wrap + text: qsTr("Only the standard, URI, creators, definition link, and primary-sale date are stored. URI content is not validated by the program.") + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: qsTr("Standard") + } + + ComboBox { + id: metadataStandardSelector + + Layout.preferredWidth: 150 + Layout.preferredHeight: 36 + Accessible.name: qsTr("Metadata standard") + currentIndex: root.metadataStandard + model: [qsTr("Simple"), qsTr("Expanded")] + + onActivated: { + root.metadataStandard = currentIndex; + root.clearDraftState(); + } + + background: Rectangle { + radius: 7 + color: "#101010" + border.color: metadataStandardSelector.activeFocus ? "#F26A21" : "#343434" + border.width: 1 + } + } + + Item { + Layout.fillWidth: true + } + } + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: qsTr("URI") + } + + TextField { + id: metadataUriField + + Layout.fillWidth: true + Layout.preferredHeight: 44 + Accessible.name: qsTr("Metadata URI") + color: "#E7E1D8" + font.pixelSize: 14 + placeholderText: qsTr("data:application/json;base64,... or external URI") + placeholderTextColor: "#8E8780" + selectByMouse: true + + onTextEdited: root.clearDraftState() + + background: Rectangle { + radius: 7 + color: "#101010" + border.color: metadataUriField.activeFocus ? "#F26A21" : "#343434" + border.width: 1 + } + } + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: qsTr("Creators") + } + + TextField { + id: creatorsField + + Layout.fillWidth: true + Layout.preferredHeight: 44 + Accessible.name: qsTr("Metadata creators string") + color: "#E7E1D8" + font.pixelSize: 14 + placeholderText: qsTr("Creator account or attribution string") + placeholderTextColor: "#8E8780" + selectByMouse: true + + onTextEdited: root.clearDraftState() + + background: Rectangle { + radius: 7 + color: "#101010" + border.color: creatorsField.activeFocus ? "#F26A21" : "#343434" + border.width: 1 + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Button { + id: examplesButton + + Layout.preferredHeight: 36 + text: qsTr("Use template") + activeFocusOnTab: true + Accessible.name: qsTr("Choose a token definition template") + onClicked: examplesMenu.popup(examplesButton, Qt.point(0, examplesButton.height)) + + contentItem: Text { + color: "#B8ADA3" + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: examplesButton.text + } + + background: Rectangle { + radius: 7 + color: "#101010" + border.color: examplesButton.activeFocus ? "#FFB26B" : "#343434" + border.width: examplesButton.activeFocus ? 2 : 1 + } + } + + Item { + Layout.fillWidth: true + } + } + + Button { + id: continueButton + + Layout.fillWidth: true + Layout.preferredHeight: visible ? 46 : 0 + activeFocusOnTab: true + Accessible.name: qsTr("Continue to account targets") + enabled: root.canContinue + text: root.canContinue ? qsTr("Continue to account targets") : qsTr("Complete required fields") + visible: stage.columnCount === 1 + onClicked: root.step = 1 + + contentItem: Text { + color: continueButton.enabled ? "#FFFFFF" : "#8E8780" + font.pixelSize: 15 + font.weight: Font.DemiBold + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: continueButton.text + } + + background: Rectangle { + radius: 8 + color: continueButton.enabled ? "#F26A21" : "#282522" + border.color: continueButton.activeFocus ? "#FFB26B" : continueButton.enabled ? "#F26A21" : "#3C3833" + border.width: continueButton.activeFocus ? 2 : 1 + } + } + } + + ColumnLayout { + id: accountsView + + Layout.fillWidth: true + Layout.preferredHeight: visible ? implicitHeight : 0 + spacing: 12 + visible: root.step === 1 + + Rectangle { + Layout.fillWidth: true + implicitHeight: accountList.implicitHeight + 24 + radius: 10 + color: "#101010" + border.color: "#343434" + border.width: 1 + + ColumnLayout { + id: accountList + + anchors.fill: parent + anchors.margins: 12 + spacing: 6 Text { color: "#E7E1D8" font.pixelSize: 15 font.weight: Font.DemiBold - text: qsTr("Metadata") + text: qsTr("What this definition creates") } Text { - color: "#A9A098" - font.pixelSize: 12 - text: root.requiresMetadata ? qsTr("Required for non-fungible definitions") : qsTr("Optional for fungible definitions") - } - } - - Switch { - id: metadataSwitch - - Layout.alignment: Qt.AlignVCenter - Accessible.name: qsTr("Include metadata account") - checked: root.hasMetadata - enabled: !root.requiresMetadata - onToggled: { - root.metadataEnabled = checked; - root.prepared = false; - } - } - } - - Text { - Layout.fillWidth: true - color: "#A9A098" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: qsTr("The program stores only standard, URI, creators, definition link, and a primary-sale date initialized to 0. It does not validate URI content or metadata schema.") - } - - ColumnLayout { - Layout.fillWidth: true - visible: root.hasMetadata - Layout.preferredHeight: visible ? implicitHeight : 0 - spacing: 7 - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("Metadata standard") - } - - Button { - id: simpleMetadataButton - - Layout.preferredHeight: 30 - text: qsTr("Simple") - checkable: true - checked: root.metadataStandard === 0 - activeFocusOnTab: true - Accessible.name: qsTr("Use Simple metadata standard") - onClicked: { - root.metadataStandard = 0; - root.prepared = false; - } - - contentItem: Text { - color: simpleMetadataButton.checked ? "#F2D8C7" : "#A9A098" - font.pixelSize: 12 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: simpleMetadataButton.text - } - - background: Rectangle { - radius: 6 - color: simpleMetadataButton.checked ? "#211914" : "#101010" - border.color: simpleMetadataButton.activeFocus ? "#FFB26B" : simpleMetadataButton.checked ? "#F26A21" : "#343434" - border.width: simpleMetadataButton.activeFocus ? 2 : 1 - } - } - - Button { - id: expandedMetadataButton - - Layout.preferredHeight: 30 - text: qsTr("Expanded") - checkable: true - checked: root.metadataStandard === 1 - activeFocusOnTab: true - Accessible.name: qsTr("Use Expanded metadata standard") - onClicked: { - root.metadataStandard = 1; - root.prepared = false; - } - - contentItem: Text { - color: expandedMetadataButton.checked ? "#F2D8C7" : "#A9A098" - font.pixelSize: 12 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: expandedMetadataButton.text - } - - background: Rectangle { - radius: 6 - color: expandedMetadataButton.checked ? "#211914" : "#101010" - border.color: expandedMetadataButton.activeFocus ? "#FFB26B" : expandedMetadataButton.checked ? "#F26A21" : "#343434" - border.width: expandedMetadataButton.activeFocus ? 2 : 1 - } - } - - Item { Layout.fillWidth: true - } - } - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("URI") - } - - TextField { - id: metadataUriField - - Layout.fillWidth: true - Layout.preferredHeight: 42 - Accessible.name: qsTr("Metadata URI") - color: "#E7E1D8" - font.pixelSize: 14 - placeholderText: qsTr("data:application/json;base64,... or external URI") - placeholderTextColor: "#8E8780" - selectByMouse: true - - onTextEdited: root.prepared = false - - background: Rectangle { - radius: 6 - color: "#101010" - border.color: metadataUriField.activeFocus ? "#F26A21" : "#343434" - border.width: 1 - } - } - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("Creators") - } - - TextField { - id: creatorsField - - Layout.fillWidth: true - Layout.preferredHeight: 42 - Accessible.name: qsTr("Metadata creators string") - color: "#E7E1D8" - font.pixelSize: 14 - placeholderText: qsTr("Creator account or attribution string") - placeholderTextColor: "#8E8780" - selectByMouse: true - - onTextEdited: root.prepared = false - - background: Rectangle { - radius: 6 - color: "#101010" - border.color: creatorsField.activeFocus ? "#F26A21" : "#343434" - border.width: 1 + color: "#A9A098" + font.pixelSize: 12 + wrapMode: Text.Wrap + text: root.hasMetadata ? qsTr("A definition account, an initial holding, and a metadata account.") : qsTr("A definition account and an initial holding.") } } } - } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: "#303030" - } + Button { + id: createAccountsButton - ColumnLayout { - Layout.fillWidth: true - spacing: 7 - - Text { - color: "#E7E1D8" - font.pixelSize: 15 - font.weight: Font.DemiBold - text: qsTr("New target accounts") - } - - Text { Layout.fillWidth: true - color: "#A9A098" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: qsTr("Every target must be a new default-valued account and authorize this transaction. The prototype can validate shape, not on-chain account state.") + Layout.preferredHeight: 38 + enabled: root.backend !== null && root.backend.isWalletOpen && !root.accountBusy + text: root.accountBusy ? qsTr("Creating fresh accounts…") : qsTr("Create fresh wallet accounts") + Accessible.name: qsTr("Create fresh wallet accounts for this definition") + onClicked: root.createTargetAccounts() + + contentItem: Text { + color: parent.enabled ? "#F2D8C7" : "#8E8780" + font.pixelSize: 13 + font.weight: Font.DemiBold + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: createAccountsButton.text + } + + background: Rectangle { + radius: 7 + color: parent.enabled ? "#211914" : "#282522" + border.color: parent.enabled ? "#49301F" : "#3C3833" + border.width: 1 + } } Text { @@ -805,25 +1085,33 @@ Item { id: definitionTargetField Layout.fillWidth: true - Layout.preferredHeight: 42 + Layout.preferredHeight: 44 Accessible.name: qsTr("Definition target account ID") clip: true color: "#E7E1D8" - font.pixelSize: 14 + font.family: "monospace" + font.pixelSize: 13 placeholderText: qsTr("New authorized account ID") placeholderTextColor: "#8E8780" selectByMouse: true - onTextEdited: root.prepared = false + onTextEdited: root.clearDraftState() background: Rectangle { - radius: 6 + radius: 7 color: "#101010" border.color: definitionTargetField.activeFocus ? "#F26A21" : definitionTargetField.text.length > 0 && !root.validDefinitionTarget ? "#D85F4B" : "#343434" border.width: 1 } } + Text { + Layout.fillWidth: true + color: definitionTargetField.text.length === 0 || root.validDefinitionTarget ? "#A9A098" : "#F08A76" + font.pixelSize: 12 + text: definitionTargetField.text.length === 0 || root.validDefinitionTarget ? qsTr("Fresh public wallet account; accepts base58 or 64-character hex.") : qsTr("Enter a base58 account ID or 64-character hex account ID.") + } + Text { color: "#A9A098" font.pixelSize: 12 @@ -834,25 +1122,33 @@ Item { id: holdingTargetField Layout.fillWidth: true - Layout.preferredHeight: 42 + Layout.preferredHeight: 44 Accessible.name: root.isFungible ? qsTr("Initial fungible holding target account ID") : qsTr("NFT master holding target account ID") clip: true color: "#E7E1D8" - font.pixelSize: 14 + font.family: "monospace" + font.pixelSize: 13 placeholderText: qsTr("New authorized account ID") placeholderTextColor: "#8E8780" selectByMouse: true - onTextEdited: root.prepared = false + onTextEdited: root.clearDraftState() background: Rectangle { - radius: 6 + radius: 7 color: "#101010" border.color: holdingTargetField.activeFocus ? "#F26A21" : holdingTargetField.text.length > 0 && !root.validHoldingTarget ? "#D85F4B" : "#343434" border.width: 1 } } + Text { + Layout.fillWidth: true + color: holdingTargetField.text.length === 0 || root.validHoldingTarget ? "#A9A098" : "#F08A76" + font.pixelSize: 12 + text: holdingTargetField.text.length === 0 || root.validHoldingTarget ? qsTr("Fresh public wallet account; receives the initial state.") : qsTr("Enter a base58 account ID or 64-character hex account ID.") + } + ColumnLayout { Layout.fillWidth: true visible: root.hasMetadata @@ -869,418 +1165,407 @@ Item { id: metadataTargetField Layout.fillWidth: true - Layout.preferredHeight: 42 + Layout.preferredHeight: 44 Accessible.name: qsTr("Metadata target account ID") clip: true color: "#E7E1D8" - font.pixelSize: 14 + font.family: "monospace" + font.pixelSize: 13 placeholderText: qsTr("New authorized account ID") placeholderTextColor: "#8E8780" selectByMouse: true - onTextEdited: root.prepared = false + onTextEdited: root.clearDraftState() background: Rectangle { - radius: 6 + radius: 7 color: "#101010" border.color: metadataTargetField.activeFocus ? "#F26A21" : metadataTargetField.text.length > 0 && !root.validMetadataTarget ? "#D85F4B" : "#343434" border.width: 1 } } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Button { - id: fixedExampleButton - - Layout.preferredHeight: 34 - text: qsTr("Fixed example") - activeFocusOnTab: true - Accessible.name: qsTr("Load fixed supply creation example") - onClicked: root.setPattern("fixed") - - contentItem: Text { - color: "#A9A098" - font.pixelSize: 12 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: fixedExampleButton.text - } - - background: Rectangle { - radius: 6 - color: "#101010" - border.color: fixedExampleButton.activeFocus ? "#FFB26B" : "#343434" - border.width: fixedExampleButton.activeFocus ? 2 : 1 - } - } - - Button { - id: metadataExampleButton - - Layout.preferredHeight: 34 - text: qsTr("Metadata example") - activeFocusOnTab: true - Accessible.name: qsTr("Load metadata token creation example") - onClicked: root.setPattern("metadata") - - contentItem: Text { - color: "#A9A098" - font.pixelSize: 12 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: metadataExampleButton.text - } - - background: Rectangle { - radius: 6 - color: "#101010" - border.color: metadataExampleButton.activeFocus ? "#FFB26B" : "#343434" - border.width: metadataExampleButton.activeFocus ? 2 : 1 - } - } - - Button { - id: nftExampleButton - - Layout.preferredHeight: 34 - text: qsTr("NFT example") - activeFocusOnTab: true - Accessible.name: qsTr("Load non-fungible collection creation example") - onClicked: root.setPattern("nft") - - contentItem: Text { - color: "#A9A098" - font.pixelSize: 12 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: nftExampleButton.text - } - - background: Rectangle { - radius: 6 - color: "#101010" - border.color: nftExampleButton.activeFocus ? "#FFB26B" : "#343434" - border.width: nftExampleButton.activeFocus ? 2 : 1 - } - } - - Item { - Layout.fillWidth: true - } - } - } - } - - Rectangle { - id: previewPanel - - Layout.alignment: Qt.AlignTop - Layout.fillWidth: true - Layout.preferredWidth: workbench.columnCount === 3 ? 410 : 0 - Layout.columnSpan: workbench.columnCount === 1 ? 1 : 1 - implicitHeight: previewContent.implicitHeight + 32 - radius: 16 - color: "#1B1B1B" - border.color: "#303030" - border.width: 1 - - ColumnLayout { - id: previewContent - - anchors.fill: parent - anchors.margins: 16 - spacing: 13 - - Text { - color: "#E7E1D8" - font.pixelSize: 18 - font.weight: Font.DemiBold - text: qsTr("Definition preview") - } - - Rectangle { - Layout.fillWidth: true - implicitHeight: previewHeadline.implicitHeight + 22 - radius: 10 - color: root.isFungible ? "#211914" : "#181D25" - border.color: root.isFungible ? "#49301F" : "#31435D" - border.width: 1 - - RowLayout { - id: previewHeadline - - anchors.fill: parent - anchors.margins: 11 - spacing: 10 - - Rectangle { - Layout.preferredHeight: 30 - Layout.preferredWidth: kindBadge.implicitWidth + 16 - radius: 15 - color: root.isFungible ? "#2D211A" : "#182534" - border.color: root.isFungible ? "#6A4329" : "#40607A" - border.width: 1 - - Text { - id: kindBadge - - anchors.centerIn: parent - color: root.isFungible ? "#F2D8C7" : "#BFD8F4" - font.pixelSize: 12 - font.weight: Font.DemiBold - text: root.isFungible ? qsTr("Fungible") : qsTr("NFT collection") - } - } Text { Layout.fillWidth: true - color: "#E7E1D8" - elide: Text.ElideRight - font.pixelSize: 16 - font.weight: Font.DemiBold - text: nameField.text.length > 0 ? nameField.text : qsTr("Untitled definition") + color: metadataTargetField.text.length === 0 || root.validMetadataTarget ? "#A9A098" : "#F08A76" + font.pixelSize: 12 + text: metadataTargetField.text.length === 0 || root.validMetadataTarget ? qsTr("Fresh public wallet account; stores the metadata record.") : qsTr("Enter a base58 account ID or 64-character hex account ID.") + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Button { + id: backToConfigureButton + + Layout.preferredWidth: 112 + Layout.preferredHeight: 46 + activeFocusOnTab: true + Accessible.name: qsTr("Back to token settings") + text: qsTr("Back") + onClicked: root.step = 0 + + contentItem: Text { + color: "#E7E1D8" + font.pixelSize: 14 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: backToConfigureButton.text + } + + background: Rectangle { + radius: 8 + color: "#101010" + border.color: backToConfigureButton.activeFocus ? "#FFB26B" : "#343434" + border.width: backToConfigureButton.activeFocus ? 2 : 1 + } + } + + Button { + id: reviewButton + + Layout.fillWidth: true + Layout.preferredHeight: 46 + activeFocusOnTab: true + Accessible.name: qsTr("Review token definition") + enabled: root.canPrepare + text: root.canPrepare ? qsTr("Review definition") : qsTr("Complete targets") + onClicked: root.step = 2 + + contentItem: Text { + color: reviewButton.enabled ? "#FFFFFF" : "#8E8780" + elide: Text.ElideRight + font.pixelSize: 15 + font.weight: Font.DemiBold + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: reviewButton.text + } + + background: Rectangle { + radius: 8 + color: reviewButton.enabled ? "#F26A21" : "#282522" + border.color: reviewButton.activeFocus ? "#FFB26B" : reviewButton.enabled ? "#F26A21" : "#3C3833" + border.width: reviewButton.activeFocus ? 2 : 1 + } } } } ColumnLayout { - Layout.fillWidth: true - spacing: 9 - - RowLayout { - Layout.fillWidth: true - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: root.isFungible ? qsTr("Stored total supply") : qsTr("Stored printable supply") - } - - Item { - Layout.fillWidth: true - } - - Text { - color: root.validSupply ? "#E7E1D8" : "#F08A76" - font.pixelSize: 14 - font.weight: Font.Medium - text: root.supplyValue - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: "#303030" - } - - RowLayout { - Layout.fillWidth: true - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("Creation instruction") - } - - Item { - Layout.fillWidth: true - } - - Text { - color: "#F2D8C7" - font.family: "monospace" - font.pixelSize: 12 - text: root.instructionName - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: "#303030" - } - - ColumnLayout { - Layout.fillWidth: true - visible: root.isFungible - Layout.preferredHeight: visible ? implicitHeight : 0 - spacing: 5 - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("Mint authority") - } - - Text { - Layout.fillWidth: true - color: "#E7E1D8" - font.pixelSize: 14 - wrapMode: Text.Wrap - text: root.authorityValue - } - } - - ColumnLayout { - Layout.fillWidth: true - visible: !root.isFungible - Layout.preferredHeight: visible ? implicitHeight : 0 - spacing: 5 - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("Master holding behavior") - } - - Text { - Layout.fillWidth: true - color: "#E7E1D8" - font.pixelSize: 14 - wrapMode: Text.Wrap - text: qsTr("Starts with print balance %1. The master remains reserved, so at most %2 printed copies are possible.").arg(root.supplyValue).arg(root.validSupply && supplyField.text !== "0" ? qsTr("one fewer than the printable supply") : qsTr("none until a positive printable supply is set")) - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: root.hasMetadata ? 1 : 0 - visible: root.hasMetadata - color: "#303030" - } - - ColumnLayout { - Layout.fillWidth: true - visible: root.hasMetadata - Layout.preferredHeight: visible ? implicitHeight : 0 - spacing: 6 - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("Metadata account") - } - - Text { - Layout.fillWidth: true - color: "#E7E1D8" - elide: Text.ElideMiddle - font.family: "monospace" - font.pixelSize: 12 - text: metadataTargetField.text.length > 0 ? metadataTargetField.text : qsTr("New metadata target required") - } - - Text { - Layout.fillWidth: true - color: "#A9A098" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: qsTr("%1 · URI and creators are stored as supplied.").arg(root.metadataStandard === 0 ? qsTr("Simple") : qsTr("Expanded")) - } - } - } - - Rectangle { - Layout.fillWidth: true - implicitHeight: holdingOutcome.implicitHeight + 24 - radius: 10 - color: "#101010" - border.color: "#343434" - border.width: 1 - - ColumnLayout { - id: holdingOutcome - - anchors.fill: parent - anchors.margins: 12 - spacing: 6 - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: root.isFungible ? qsTr("Initial fungible holding") : qsTr("Initial NFT master holding") - } - - Text { - Layout.fillWidth: true - color: "#E7E1D8" - elide: Text.ElideMiddle - font.family: "monospace" - font.pixelSize: 12 - text: holdingTargetField.text.length > 0 ? holdingTargetField.text : qsTr("New holding target required") - } - - Text { - Layout.fillWidth: true - color: "#E7E1D8" - font.pixelSize: 14 - wrapMode: Text.Wrap - text: root.isFungible ? qsTr("Receives the full initial raw supply: %1.").arg(root.supplyValue) : qsTr("Receives the master state and print balance: %1.").arg(root.supplyValue) - } - } - } - - Button { - id: prepareButton + id: reviewView Layout.fillWidth: true - Layout.preferredHeight: 46 - activeFocusOnTab: true - Accessible.name: qsTr("Prepare token definition draft") - enabled: root.canPrepare - text: root.canPrepare ? qsTr("Prepare definition") : qsTr("Complete required targets") - onClicked: root.prepareDefinition() - - contentItem: Text { - color: prepareButton.enabled ? "#FFFFFF" : "#8E8780" - font.pixelSize: 15 - font.weight: Font.DemiBold - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: prepareButton.text - } - - background: Rectangle { - radius: 8 - color: prepareButton.enabled ? "#F26A21" : "#282522" - border.color: prepareButton.activeFocus ? "#FFB26B" : prepareButton.enabled ? "#F26A21" : "#3C3833" - border.width: prepareButton.activeFocus ? 2 : 1 - } - } - - Text { - Layout.fillWidth: true - visible: root.prepared Layout.preferredHeight: visible ? implicitHeight : 0 - color: "#78C88D" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: root.preparedMessage + spacing: 12 + visible: root.step === 2 + + Rectangle { + Layout.fillWidth: true + implicitHeight: reviewContent.implicitHeight + 24 + radius: 10 + color: "#101010" + border.color: "#343434" + border.width: 1 + + ColumnLayout { + id: reviewContent + + anchors.fill: parent + anchors.margins: 12 + spacing: 9 + + RowLayout { + Layout.fillWidth: true + + Rectangle { + Layout.preferredWidth: 78 + Layout.preferredHeight: 28 + radius: 14 + color: root.isFungible ? "#211914" : "#182534" + border.color: root.isFungible ? "#6A4329" : "#40607A" + border.width: 1 + + Text { + anchors.centerIn: parent + color: root.isFungible ? "#F2D8C7" : "#BFD8F4" + font.pixelSize: 12 + font.weight: Font.DemiBold + text: root.isFungible ? qsTr("Fungible") : qsTr("NFT") + } + } + + Text { + Layout.fillWidth: true + color: "#E7E1D8" + elide: Text.ElideRight + font.pixelSize: 18 + font.weight: Font.DemiBold + text: nameField.text.length > 0 ? nameField.text : qsTr("Untitled definition") + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: "#303030" + } + + RowLayout { + Layout.fillWidth: true + + Text { + color: "#A9A098" + font.pixelSize: 13 + text: root.supplyLabel + } + + Item { + Layout.fillWidth: true + } + + Text { + color: root.validSupply ? "#E7E1D8" : "#F08A76" + font.pixelSize: 14 + font.weight: Font.Medium + text: root.supplyValue + } + } + + RowLayout { + Layout.fillWidth: true + + Text { + color: "#A9A098" + font.pixelSize: 13 + text: root.isFungible ? qsTr("Mint authority") : qsTr("Printing") + } + + Item { + Layout.fillWidth: true + } + + Text { + Layout.maximumWidth: 280 + color: "#E7E1D8" + elide: Text.ElideRight + font.pixelSize: 13 + text: root.authorityValue + horizontalAlignment: Text.AlignRight + } + } + + RowLayout { + Layout.fillWidth: true + + Text { + color: "#A9A098" + font.pixelSize: 13 + text: qsTr("Metadata") + } + + Item { + Layout.fillWidth: true + } + + Text { + color: "#E7E1D8" + font.pixelSize: 13 + text: root.hasMetadata ? root.metadataStandard === 0 ? qsTr("Simple") : qsTr("Expanded") : qsTr("None") + } + } + } + } + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: qsTr("Account targets") + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + + Repeater { + model: [ + { label: qsTr("Definition"), value: definitionTargetField.text, ok: root.validDefinitionTarget, metadata: false }, + { label: root.isFungible ? qsTr("Initial holding") : qsTr("Master holding"), value: holdingTargetField.text, ok: root.validHoldingTarget, metadata: false }, + { label: qsTr("Metadata"), value: metadataTargetField.text, ok: root.validMetadataTarget, metadata: true } + ] + + delegate: RowLayout { + id: reviewTargetRow + + required property var modelData + + Layout.fillWidth: true + visible: !reviewTargetRow.modelData.metadata || root.hasMetadata + spacing: 8 + + Text { + Layout.preferredWidth: 112 + color: "#A9A098" + font.pixelSize: 12 + text: reviewTargetRow.modelData.label + } + + Text { + Layout.fillWidth: true + color: "#E7E1D8" + elide: Text.ElideMiddle + font.family: "monospace" + font.pixelSize: 12 + text: reviewTargetRow.modelData.value.length > 0 ? reviewTargetRow.modelData.value : qsTr("Missing") + } + + Text { + color: reviewTargetRow.modelData.value.length > 0 && reviewTargetRow.modelData.ok ? "#78C88D" : "#F08A76" + font.pixelSize: 12 + text: reviewTargetRow.modelData.value.length > 0 && reviewTargetRow.modelData.ok ? qsTr("Ready") : qsTr("Needs input") + } + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: reviewNotice.implicitHeight + 20 + radius: 8 + color: "#211914" + border.color: "#49301F" + border.width: 1 + + Text { + id: reviewNotice + + anchors.fill: parent + anchors.margins: 10 + color: "#F2D8C7" + font.pixelSize: 12 + wrapMode: Text.Wrap + text: root.hasMetadata ? qsTr("Metadata-backed definitions use typed serialization.") : qsTr("Fungible definitions use the standard Token Program fields.") + } + } + + Text { + Layout.fillWidth: true + visible: root.prepared || root.errorMessage.length > 0 + color: root.errorMessage.length > 0 ? "#F08A76" : "#78C88D" + font.pixelSize: 13 + wrapMode: Text.Wrap + text: root.errorMessage.length > 0 ? root.errorMessage : root.preparedMessage + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Button { + id: backToAccountsButton + + Layout.preferredWidth: 112 + Layout.preferredHeight: 46 + activeFocusOnTab: true + Accessible.name: qsTr("Back to account targets") + enabled: !root.prepared && !root.submitting + text: qsTr("Back") + onClicked: root.step = 1 + + contentItem: Text { + color: backToAccountsButton.enabled ? "#E7E1D8" : "#8E8780" + font.pixelSize: 14 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: backToAccountsButton.text + } + + background: Rectangle { + radius: 8 + color: "#101010" + border.color: backToAccountsButton.activeFocus ? "#FFB26B" : "#343434" + border.width: backToAccountsButton.activeFocus ? 2 : 1 + } + } + + Button { + id: prepareButton + + Layout.fillWidth: true + Layout.preferredHeight: 46 + activeFocusOnTab: true + Accessible.name: qsTr("Create token definition") + enabled: root.canSubmit + text: root.submitting ? qsTr("Submitting…") : root.prepared ? qsTr("Transaction submitted") : !root.backend || !root.backend.isWalletOpen ? qsTr("Connect wallet to create") : root.canPrepare ? qsTr("Create token definition") : qsTr("Complete required fields") + onClicked: root.prepareDefinition() + + contentItem: Text { + color: prepareButton.enabled ? "#FFFFFF" : root.prepared ? "#78C88D" : "#8E8780" + font.pixelSize: 15 + font.weight: Font.DemiBold + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: prepareButton.text + } + + background: Rectangle { + radius: 8 + color: prepareButton.enabled ? "#F26A21" : root.prepared ? "#183222" : "#282522" + border.color: prepareButton.activeFocus ? "#FFB26B" : prepareButton.enabled ? "#F26A21" : root.prepared ? "#39C06A" : "#3C3833" + border.width: prepareButton.activeFocus ? 2 : 1 + } + } + } + + Button { + id: inspectButton + + Layout.fillWidth: true + Layout.preferredHeight: 42 + visible: root.prepared + activeFocusOnTab: true + Accessible.name: qsTr("Inspect token definition") + text: qsTr("Open in Inspect") + onClicked: root.requestInspect() + + contentItem: Text { + color: "#F2D8C7" + font.pixelSize: 14 + font.weight: Font.DemiBold + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: inspectButton.text + } + + background: Rectangle { + radius: 8 + color: "#211914" + border.color: inspectButton.activeFocus ? "#FFB26B" : "#49301F" + border.width: inspectButton.activeFocus ? 2 : 1 + } + } } } } Rectangle { - id: readinessPanel + id: summaryPanel Layout.alignment: Qt.AlignTop Layout.fillWidth: true - Layout.preferredWidth: workbench.columnCount === 3 ? 300 : 0 - Layout.columnSpan: workbench.columnCount === 1 ? 1 : workbench.columnCount === 2 ? 2 : 1 - implicitHeight: readinessContent.implicitHeight + 32 + Layout.minimumWidth: 0 + Layout.preferredWidth: stage.columnCount === 2 ? 300 : 0 + Layout.maximumWidth: 1120 + implicitHeight: summaryContent.implicitHeight + 32 radius: 16 color: "#181818" border.color: "#303030" border.width: 1 ColumnLayout { - id: readinessContent + id: summaryContent anchors.fill: parent anchors.margins: 16 @@ -1290,15 +1575,109 @@ Item { color: "#E7E1D8" font.pixelSize: 18 font.weight: Font.DemiBold - text: qsTr("Protocol readiness") + text: qsTr("Definition summary") } - Text { + Rectangle { Layout.fillWidth: true - color: "#A9A098" - font.pixelSize: 13 - wrapMode: Text.Wrap - text: qsTr("Creation requires fresh authorized target accounts. Green checks confirm form shape only; account state remains unverified until a real client reads the chain.") + implicitHeight: summaryHeadline.implicitHeight + 20 + radius: 10 + color: root.isFungible ? "#211914" : "#182534" + border.color: root.isFungible ? "#49301F" : "#31435D" + border.width: 1 + + ColumnLayout { + id: summaryHeadline + + anchors.fill: parent + anchors.margins: 10 + spacing: 5 + + Text { + color: root.isFungible ? "#F2D8C7" : "#BFD8F4" + font.pixelSize: 12 + font.weight: Font.DemiBold + text: root.isFungible ? qsTr("Fungible") : qsTr("Non-fungible") + } + + Text { + Layout.fillWidth: true + color: "#E7E1D8" + elide: Text.ElideRight + font.pixelSize: 18 + font.weight: Font.DemiBold + text: nameField.text.length > 0 ? nameField.text : qsTr("Untitled definition") + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 8 + + RowLayout { + Layout.fillWidth: true + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: root.supplyLabel + } + + Item { + Layout.fillWidth: true + } + + Text { + color: root.validSupply ? "#E7E1D8" : "#F08A76" + font.pixelSize: 13 + font.weight: Font.Medium + text: root.supplyValue + } + } + + RowLayout { + Layout.fillWidth: true + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: root.isFungible ? qsTr("Authority") : qsTr("Printing") + } + + Item { + Layout.fillWidth: true + } + + Text { + Layout.maximumWidth: 170 + color: "#E7E1D8" + elide: Text.ElideRight + font.pixelSize: 12 + text: root.authorityValue + horizontalAlignment: Text.AlignRight + } + } + + RowLayout { + Layout.fillWidth: true + + Text { + color: "#A9A098" + font.pixelSize: 12 + text: qsTr("Metadata") + } + + Item { + Layout.fillWidth: true + } + + Text { + color: "#E7E1D8" + font.pixelSize: 12 + text: root.hasMetadata ? root.metadataStandard === 0 ? qsTr("Simple") : qsTr("Expanded") : qsTr("Not included") + } + } } Rectangle { @@ -1307,154 +1686,84 @@ Item { color: "#303030" } - Repeater { - model: [ - { - label: qsTr("Definition target"), - detail: qsTr("init · signer · writable"), - ok: root.validDefinitionTarget - }, - { - label: root.isFungible ? qsTr("Initial holding") : qsTr("NFT master holding"), - detail: qsTr("init · signer · writable"), - ok: root.validHoldingTarget - }, - { - label: qsTr("Metadata target"), - detail: root.hasMetadata ? qsTr("init · signer · writable") : qsTr("not used by this instruction"), - ok: root.hasMetadata ? root.validMetadataTarget : true - } - ] - - delegate: RowLayout { - id: readinessRow - - required property var modelData - - Layout.fillWidth: true - spacing: 9 - - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: readinessState.implicitWidth + 14 - radius: 12 - color: readinessRow.modelData.ok ? "#183222" : "#332521" - border.color: readinessRow.modelData.ok ? "#39C06A" : "#D85F4B" - border.width: 1 - - Text { - id: readinessState - - anchors.centerIn: parent - color: readinessRow.modelData.ok ? "#78C88D" : "#F08A76" - font.pixelSize: 11 - font.weight: Font.DemiBold - text: readinessRow.modelData.ok ? qsTr("Ready") : qsTr("Needs input") - } - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - color: "#E7E1D8" - font.pixelSize: 14 - font.weight: Font.Medium - text: readinessRow.modelData.label - } - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: readinessRow.modelData.detail - } - } - } + Text { + color: "#E7E1D8" + font.pixelSize: 14 + font.weight: Font.DemiBold + text: qsTr("Readiness") } - ColumnLayout { + RowLayout { Layout.fillWidth: true - visible: root.isFungible && root.authorityMode === 2 - Layout.preferredHeight: visible ? implicitHeight : 0 - spacing: 7 + spacing: 8 Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: "#303030" + Layout.preferredWidth: 28 + Layout.preferredHeight: 28 + radius: 14 + color: root.validTargetCount === root.targetCount ? "#183222" : "#332521" + border.color: root.validTargetCount === root.targetCount ? "#39C06A" : "#D85F4B" + border.width: 1 + + Text { + anchors.centerIn: parent + color: root.validTargetCount === root.targetCount ? "#78C88D" : "#F08A76" + font.pixelSize: 12 + font.weight: Font.DemiBold + text: root.validTargetCount === root.targetCount ? qsTr("OK") : qsTr("!") + } } - RowLayout { + ColumnLayout { Layout.fillWidth: true - spacing: 9 + spacing: 2 - Rectangle { - Layout.preferredHeight: 24 - Layout.preferredWidth: externalAuthorityState.implicitWidth + 14 - radius: 12 - color: root.validExternalAuthority ? "#183222" : "#332521" - border.color: root.validExternalAuthority ? "#39C06A" : "#D85F4B" - border.width: 1 - - Text { - id: externalAuthorityState - - anchors.centerIn: parent - color: root.validExternalAuthority ? "#78C88D" : "#F08A76" - font.pixelSize: 11 - font.weight: Font.DemiBold - text: root.validExternalAuthority ? qsTr("Ready") : qsTr("Needs input") - } + Text { + color: "#E7E1D8" + font.pixelSize: 13 + font.weight: Font.Medium + text: root.validTargetCount === root.targetCount ? qsTr("All targets look valid") : qsTr("%1 of %2 targets ready").arg(root.validTargetCount).arg(root.targetCount) } - ColumnLayout { + Text { Layout.fillWidth: true - spacing: 2 - - Text { - color: "#E7E1D8" - font.pixelSize: 14 - font.weight: Font.Medium - text: qsTr("External mint authority") - } - - Text { - color: "#A9A098" - font.pixelSize: 12 - text: qsTr("Must be a non-zero account ID") - } + color: "#A9A098" + font.pixelSize: 12 + wrapMode: Text.Wrap + text: root.step === 0 ? qsTr("Account targets appear next.") : root.step === 1 ? qsTr("Review unlocks when every target is valid.") : root.prepared ? qsTr("Transaction submitted to the Token Program.") : qsTr("Ready for final review.") } } } - Rectangle { + Button { + id: summaryContinueButton + Layout.fillWidth: true - implicitHeight: unsupportedNotice.implicitHeight + 22 - radius: 8 - color: "#211914" - border.color: "#49301F" - border.width: 1 + Layout.preferredHeight: visible ? 46 : 0 + visible: root.step === 0 && stage.columnCount === 2 + activeFocusOnTab: true + Accessible.name: qsTr("Continue to account targets") + enabled: root.canContinue + text: root.canContinue ? qsTr("Continue to account targets") : qsTr("Complete required fields") + onClicked: root.step = 1 - Text { - id: unsupportedNotice + contentItem: Text { + color: summaryContinueButton.enabled ? "#FFFFFF" : "#8E8780" + font.pixelSize: 14 + font.weight: Font.DemiBold + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: summaryContinueButton.text + } - anchors.fill: parent - anchors.margins: 11 - color: "#F2D8C7" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: root.hasMetadata ? qsTr("Metadata-backed definitions require typed serialization today; the generic IDL route cannot serialize the structured creation arguments.") : qsTr("This simple fungible path maps to the generic creation instruction, but this prototype never submits it.") + background: Rectangle { + radius: 8 + color: summaryContinueButton.enabled ? "#F26A21" : "#282522" + border.color: summaryContinueButton.activeFocus ? "#FFB26B" : summaryContinueButton.enabled ? "#F26A21" : "#3C3833" + border.width: summaryContinueButton.activeFocus ? 2 : 1 } } - Text { - Layout.fillWidth: true - color: "#8E8780" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: qsTr("No on-chain symbol, decimal field, image, royalty, collection, or mutable metadata setting exists in the current token definition schema.") - } } } } diff --git a/apps/token/qml/pages/ManagePage.qml b/apps/token/qml/pages/ManagePage.qml index f363ce0..7677529 100644 --- a/apps/token/qml/pages/ManagePage.qml +++ b/apps/token/qml/pages/ManagePage.qml @@ -1,20 +1,21 @@ -/* - * Historical-definition inspector. This is intentionally a read model: the - * testnet snapshot and locally prepared drafts never trigger a chain query. - */ pragma ComponentBehavior: Bound import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick.Controls.Basic import QtQuick.Layouts 1.15 Item { id: root property var store: null + property var backend: null + property var runtime: null property string query: "" property string typeFilter: "all" property string selectedId: "" + property bool loading: false + property string loadError: "" + property int refreshSerial: 0 readonly property var definitions: store ? store.allDefinitions : [] readonly property var filteredDefinitions: root.filterDefinitions() @@ -71,7 +72,13 @@ Item { function sourceLabel(definition) { if (!definition) return ""; - return definition.source === "draft" ? qsTr("Local draft") : qsTr("Testnet snapshot · 12 Jul 2026"); + if (definition.source === "pending") + return qsTr("Pending transaction"); + if (definition.source === "network") + return qsTr("Network"); + if (definition.source === "draft") + return qsTr("Draft"); + return qsTr("Example"); } function authorityTitle(definition) { @@ -98,14 +105,61 @@ Item { if (definition.authorityMode === "self") return qsTr("The definition account is the mint authority."); if (definition.authorityMode === "renounced") - return qsTr("This historical definition exercised authority changes, then ended with no authority."); + return qsTr("No mint authority is stored; the supply cannot be increased."); return qsTr("No mint authority is stored; the supply cannot be increased."); } - Component.onCompleted: root.ensureSelection() + function refreshLiveDefinitions() { + if (!root.store) + return; + + if (!root.backend || !root.backend.isWalletOpen || !root.runtime) { + root.loading = false; + root.loadError = ""; + if (root.store.clearLiveDefinitions) + root.store.clearLiveDefinitions(); + root.ensureSelection(); + return; + } + + var requestSerial = ++root.refreshSerial; + root.loading = true; + root.loadError = ""; + root.runtime.watch(root.backend.walletDefinitions(), function(definitions) { + if (requestSerial !== root.refreshSerial) + return; + root.loading = false; + root.store.setLiveDefinitions(definitions || []); + root.ensureSelection(); + }, function(error) { + if (requestSerial !== root.refreshSerial) + return; + root.loading = false; + root.loadError = qsTr("Token accounts could not be read: %1").arg(error); + root.store.clearLiveDefinitions(); + root.ensureSelection(); + }); + } + + onVisibleChanged: { + if (visible) + root.refreshLiveDefinitions(); + } + onBackendChanged: root.refreshLiveDefinitions() + onRuntimeChanged: root.refreshLiveDefinitions() + Component.onCompleted: { + root.ensureSelection(); + root.refreshLiveDefinitions(); + } onStoreChanged: root.ensureSelection() onFilteredDefinitionsChanged: root.ensureSelection() + Connections { + target: root.backend + ignoreUnknownSignals: true + function onIsWalletOpenChanged() { root.refreshLiveDefinitions() } + } + Rectangle { anchors.fill: parent color: "#151515" @@ -117,58 +171,114 @@ Item { anchors.fill: parent clip: true contentWidth: width - contentHeight: content.implicitHeight + 48 + contentHeight: content.implicitHeight + 32 flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { + id: verticalScrollBar + + parent: scroll.parent + anchors.top: scroll.top + anchors.right: scroll.right + anchors.bottom: scroll.bottom + anchors.topMargin: 4 + anchors.rightMargin: 4 + anchors.bottomMargin: 4 + policy: scroll.contentHeight > scroll.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff + active: true + visible: policy === ScrollBar.AlwaysOn + width: 12 + z: 10 + + background: Rectangle { + radius: 6 + color: "#292929" + } + + contentItem: Rectangle { + implicitWidth: 8 + implicitHeight: 32 + radius: 4 + color: verticalScrollBar.pressed ? "#F26A21" : "#9A8C81" + opacity: 1 + } + } + ColumnLayout { id: content - width: Math.max(280, Math.min(scroll.width - 40, 1440)) - x: Math.max(20, (scroll.width - width) / 2) + width: Math.max(240, Math.min(scroll.width - 32, 1440)) + x: Math.max(16, (scroll.width - width) / 2) y: 24 spacing: 18 - RowLayout { + ColumnLayout { + id: pageHeader + Layout.fillWidth: true - spacing: 18 + spacing: 5 ColumnLayout { Layout.fillWidth: true + Layout.minimumWidth: 0 spacing: 5 Text { + Layout.fillWidth: true + Layout.minimumWidth: 0 color: "#E7E1D8" font.pixelSize: 28 font.weight: Font.DemiBold + wrapMode: Text.Wrap text: qsTr("Inspect token definitions") } Text { Layout.fillWidth: true + Layout.minimumWidth: 0 color: "#A9A098" font.pixelSize: 14 wrapMode: Text.Wrap - text: qsTr("Read the deployed definition state and locally prepared drafts. Testnet data is a historical reference, not a live chain response.") + text: qsTr("View deployed token definitions and account state.") } - } - Rectangle { - Layout.alignment: Qt.AlignTop - Layout.preferredHeight: 28 - Layout.preferredWidth: snapshotLabel.implicitWidth + 18 - radius: 14 - color: "#182534" - border.color: "#40607A" - border.width: 1 + RowLayout { + Layout.fillWidth: true + spacing: 8 - Text { - id: snapshotLabel + Text { + Layout.fillWidth: true + color: root.loadError.length > 0 ? "#F08A76" : "#8E8780" + font.pixelSize: 12 + elide: Text.ElideRight + text: root.loadError.length > 0 ? root.loadError : root.loading ? qsTr("Reading wallet token accounts…") : root.backend && root.backend.isWalletOpen ? qsTr("Live wallet view") : qsTr("Connect wallet to inspect live assets") + } - anchors.centerIn: parent - color: "#BFD8F4" - font.pixelSize: 12 - font.weight: Font.DemiBold - text: qsTr("Read-only snapshot") + Button { + id: refreshDefinitionsButton + + Layout.preferredWidth: 86 + Layout.preferredHeight: 32 + enabled: root.backend !== null && root.backend.isWalletOpen && !root.loading + text: root.loading ? qsTr("Reading…") : qsTr("Refresh") + Accessible.name: qsTr("Refresh live token definitions") + onClicked: root.refreshLiveDefinitions() + + contentItem: Text { + color: parent.enabled ? "#F2D8C7" : "#8E8780" + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: refreshDefinitionsButton.text + } + + background: Rectangle { + radius: 7 + color: parent.enabled ? "#211914" : "#282522" + border.color: parent.enabled ? "#49301F" : "#3C3833" + border.width: 1 + } + } } } } @@ -176,9 +286,10 @@ Item { GridLayout { id: workbench - readonly property int columnCount: content.width >= 1100 ? 2 : 1 + readonly property int columnCount: content.width >= 1180 ? 2 : 1 Layout.fillWidth: true + Layout.minimumWidth: 0 columns: columnCount columnSpacing: 14 rowSpacing: 14 @@ -188,7 +299,9 @@ Item { Layout.alignment: Qt.AlignTop Layout.fillWidth: true + Layout.minimumWidth: 0 Layout.preferredWidth: workbench.columnCount === 2 ? 390 : 0 + Layout.maximumWidth: workbench.columnCount === 2 ? 430 : 1440 implicitHeight: indexContent.implicitHeight + 32 radius: 16 color: "#1B1B1B" @@ -423,9 +536,9 @@ Item { } Text { - color: definitionRow.modelData.source === "draft" ? "#78C88D" : "#A9A098" + color: definitionRow.modelData.source === "pending" || definitionRow.modelData.source === "draft" ? "#78C88D" : "#A9A098" font.pixelSize: 11 - text: definitionRow.modelData.source === "draft" ? qsTr("Draft") : qsTr("Testnet") + text: root.sourceLabel(definitionRow.modelData) } } } @@ -441,25 +554,6 @@ Item { } } - Rectangle { - Layout.fillWidth: true - implicitHeight: indexFootnote.implicitHeight + 20 - radius: 8 - color: "#181818" - border.color: "#303030" - border.width: 1 - - Text { - id: indexFootnote - - anchors.fill: parent - anchors.margins: 10 - color: "#8E8780" - font.pixelSize: 12 - wrapMode: Text.Wrap - text: qsTr("A prepared draft joins this index locally; it is not an account, does not reserve an address, and is never sent to testnet.") - } - } } } @@ -468,6 +562,9 @@ Item { Layout.alignment: Qt.AlignTop Layout.fillWidth: true + Layout.minimumWidth: 0 + Layout.preferredWidth: workbench.columnCount === 2 ? 850 : 0 + Layout.maximumWidth: 1440 implicitHeight: detailContent.implicitHeight + 32 radius: 16 color: "#1B1B1B" @@ -527,7 +624,7 @@ Item { Text { Layout.fillWidth: true - color: root.hasSelection && root.selectedDefinition.source === "draft" ? "#78C88D" : "#A9A098" + color: root.hasSelection && (root.selectedDefinition.source === "pending" || root.selectedDefinition.source === "draft") ? "#78C88D" : "#A9A098" font.pixelSize: 12 text: root.hasSelection ? root.sourceLabel(root.selectedDefinition) : "" } @@ -855,7 +952,7 @@ Item { Layout.preferredHeight: visible ? implicitHeight : 0 color: "#A9A098" font.pixelSize: 12 - text: root.hasSelection ? root.selectedDefinition.authorityLabel : "" + text: root.hasSelection ? String(root.selectedDefinition.authorityLabel || "") : "" } Text { @@ -955,7 +1052,7 @@ Item { color: "#A9A098" font.pixelSize: 12 wrapMode: Text.Wrap - text: root.hasSelection && root.selectedDefinition.source === "draft" ? qsTr("A prepared draft has an intended initial holding target but no observed account state.") : qsTr("Balances and ownership shown here belong to the supplied historical snapshot.") + text: root.hasSelection && root.selectedDefinition.source === "pending" ? qsTr("Transaction accepted; refresh after the account state is indexed.") : qsTr("Balances and ownership reflect the selected network state.") } Column { diff --git a/apps/token/qml/state/TokenPrototypeStore.qml b/apps/token/qml/state/TokenStore.qml similarity index 98% rename from apps/token/qml/state/TokenPrototypeStore.qml rename to apps/token/qml/state/TokenStore.qml index a8d0823..3d32d57 100644 --- a/apps/token/qml/state/TokenPrototypeStore.qml +++ b/apps/token/qml/state/TokenStore.qml @@ -621,7 +621,11 @@ QtObject { ] property var draftDefinitions: [] - readonly property var allDefinitions: root.fixtureDefinitions.concat(root.draftDefinitions) + property var liveDefinitions: [] + property bool liveDefinitionsLoaded: false + readonly property var allDefinitions: root.liveDefinitionsLoaded + ? root.liveDefinitions.concat(root.draftDefinitions) + : root.fixtureDefinitions.concat(root.draftDefinitions) function findDefinition(id) { var wantedId = String(id || "") @@ -670,4 +674,14 @@ QtObject { root.draftDefinitions = root.draftDefinitions.concat([draft]) return draft } + + function setLiveDefinitions(definitions) { + root.liveDefinitions = definitions || [] + root.liveDefinitionsLoaded = true + } + + function clearLiveDefinitions() { + root.liveDefinitions = [] + root.liveDefinitionsLoaded = false + } } diff --git a/apps/token/src/TokenUiBackend.cpp b/apps/token/src/TokenUiBackend.cpp index 35d5eed..ba8b4b9 100644 --- a/apps/token/src/TokenUiBackend.cpp +++ b/apps/token/src/TokenUiBackend.cpp @@ -1,6 +1,451 @@ #include "TokenUiBackend.h" -// TokenUiBackend is header-only: its constructor and slot forwarders are inline, -// and all wallet behaviour lives in the shared WalletBackendLogic CRTP base -// (apps/common/wallet-ui). This translation unit exists so AUTOMOC compiles the -// generated moc for TokenUiBackend and its vtable is emitted here. +#include +#include +#include +#include + +#include "LogosWalletProvider.h" +#include "WalletController.h" +#include "logos_api.h" +#include "logos_sdk.h" + +namespace { + +QVariantMap errorResult(const QString& error) +{ + return { + { QStringLiteral("status"), QStringLiteral("error") }, + { QStringLiteral("error"), error }, + }; +} + +bool succeeded(const QVariantMap& result) +{ + return result.value(QStringLiteral("status")).toString() == QStringLiteral("ok"); +} + +QString displayMetadataStandard(const QString& standard) +{ + if (standard.isEmpty()) + return {}; + QString display = standard.toLower(); + display[0] = display.at(0).toUpper(); + return display; +} + +QVariantMap normalizeHolding(const QVariantMap& account) +{ + const QString kind = account.value(QStringLiteral("kind")).toString(); + QVariantMap holding { + { QStringLiteral("id"), account.value(QStringLiteral("accountId")) }, + { QStringLiteral("wallet"), QStringLiteral("connected wallet") }, + { QStringLiteral("role"), kind }, + }; + + if (kind == QStringLiteral("fungible")) { + const QString balance = account.value(QStringLiteral("balanceRaw")).toString(); + holding.insert(QStringLiteral("rawBalance"), balance); + holding.insert(QStringLiteral("displayBalance"), balance); + } else if (kind == QStringLiteral("nftMaster")) { + holding.insert(QStringLiteral("printBalance"), + account.value(QStringLiteral("printBalanceRaw")).toString()); + } else if (kind == QStringLiteral("nftPrintedCopy")) { + holding.insert(QStringLiteral("owned"), account.value(QStringLiteral("owned"))); + } + + return holding; +} + +} + +TokenUiBackend::TokenUiBackend(LogosAPI* logosAPI, QObject* parent) + : TokenUiBackendSimpleSource(parent), + m_logosAPI(logosAPI ? logosAPI : new LogosAPI("token_ui", this)), + m_logos(std::make_unique(m_logosAPI)), + m_wallet(std::make_unique(m_logosAPI)), + m_walletController(std::make_unique( + *m_wallet, QStringLiteral("TokenUI"))) +{ + connect(m_walletController.get(), &WalletController::stateChanged, + this, &TokenUiBackend::syncWalletState); + syncWalletState(); + m_walletController->start(); +} + +TokenUiBackend::~TokenUiBackend() = default; + +WalletAccountModel* TokenUiBackend::accountModel() const +{ + return m_walletController->accountModel(); +} + +QString TokenUiBackend::createAccountPublic() +{ + return m_walletController->createAccount(true); +} + +QString TokenUiBackend::createAccountPrivate() +{ + return m_walletController->createAccount(false); +} + +void TokenUiBackend::refreshAccounts() +{ + m_walletController->refresh(); +} + +void TokenUiBackend::refreshBalances() +{ + m_walletController->refresh(); +} + +QString TokenUiBackend::getBalance(QString accountIdHex, bool isPublic) +{ + return m_walletController->balance(accountIdHex, isPublic); +} + +QString TokenUiBackend::createNewDefault(QString password) +{ + const QString mnemonic = m_walletController->createDefaultWallet(password); + syncWalletState(); + return mnemonic; +} + +QString TokenUiBackend::createNew(QString configPath, QString storagePath, QString password) +{ + const QString mnemonic = m_walletController->createWallet( + configPath, storagePath, password); + syncWalletState(); + return mnemonic; +} + +bool TokenUiBackend::openExisting() +{ + const bool opened = m_walletController->open(); + syncWalletState(); + return opened; +} + +void TokenUiBackend::disconnectWallet() +{ + m_walletController->disconnect(); + syncWalletState(); +} + +void TokenUiBackend::syncWalletState() +{ + const WalletUiState& state = m_walletController->state(); + setIsWalletOpen(state.isWalletOpen); + setWalletExists(state.walletExists); + setConfigPath(state.configPath); + setStoragePath(state.storagePath); + setWalletHome(state.walletHome); + setLastSyncedBlock(state.lastSyncedBlock); + setCurrentBlockHeight(state.currentBlockHeight); + setSequencerAddr(state.sequencerAddress); + setSequencerReachable(state.sequencerReachable); +} + +QVariantMap TokenUiBackend::walletUnavailable() const +{ + return errorResult(QStringLiteral("wallet_unavailable")); +} + +QVariantMap TokenUiBackend::refreshAfterSubmit(QVariantMap result) +{ + if (succeeded(result) + && !result.value(QStringLiteral("transactionId")).toString().isEmpty()) { + m_walletController->refresh(); + } + return result; +} + +QVariantMap TokenUiBackend::tokenProgramInfo() +{ + return m_logos->token_module.programInfo(); +} + +QVariantMap TokenUiBackend::inspectDefinition(QString definitionId) +{ + return m_logos->token_module.inspectDefinition(definitionId); +} + +QVariantMap TokenUiBackend::inspectHolding(QString holdingId) +{ + return m_logos->token_module.inspectHolding(holdingId); +} + +QVariantMap TokenUiBackend::inspectMetadata(QString metadataId) +{ + return m_logos->token_module.inspectMetadata(metadataId); +} + +QVariantMap TokenUiBackend::walletTokenAccounts() +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return m_logos->token_module.walletTokenAccounts(); +} + +QVariantList TokenUiBackend::walletDefinitions() +{ + if (!m_walletController->state().isWalletOpen) + return {}; + + const QVariantMap accountsResult = m_logos->token_module.walletTokenAccounts(); + if (!succeeded(accountsResult)) + return {}; + + QMap definitionIds; + QMap holdingsByDefinition; + const QVariantList accounts = accountsResult.value(QStringLiteral("accounts")).toList(); + for (const QVariant& value : accounts) { + const QVariantMap account = value.toMap(); + const QString accountType = account.value(QStringLiteral("accountType")).toString(); + const QString accountId = account.value(QStringLiteral("accountId")).toString(); + const QString accountHex = account.value(QStringLiteral("accountIdHex")).toString(); + + if (accountType == QStringLiteral("definition")) { + const QString key = accountHex.isEmpty() ? accountId : accountHex; + if (!key.isEmpty()) + definitionIds.insert(key, accountId); + continue; + } + + if (accountType != QStringLiteral("holding")) + continue; + const QString definitionId = account.value(QStringLiteral("definitionId")).toString(); + const QString definitionHex = account.value(QStringLiteral("definitionIdHex")).toString(); + const QString key = definitionHex.isEmpty() ? definitionId : definitionHex; + if (key.isEmpty()) + continue; + if (!definitionIds.contains(key)) + definitionIds.insert(key, definitionId); + holdingsByDefinition[key].append(account); + } + + QVariantList records; + for (auto definition = definitionIds.cbegin(); definition != definitionIds.cend(); ++definition) { + const QVariantMap inspected = m_logos->token_module.inspectDefinition(definition.value()); + if (!succeeded(inspected)) + continue; + + const QVariantMap raw = inspected.value(QStringLiteral("definition")).toMap(); + if (raw.isEmpty()) + continue; + + const QString kind = raw.value(QStringLiteral("kind")).toString(); + const bool fungible = kind == QStringLiteral("fungible"); + const QString id = raw.value(QStringLiteral("accountId")).toString(); + const QString idHex = raw.value(QStringLiteral("accountIdHex")).toString(); + const QString metadataId = raw.value(QStringLiteral("metadataId")).toString(); + const QString authority = raw.value(QStringLiteral("mintAuthorityId")).toString(); + + QVariantMap record { + { QStringLiteral("id"), id }, + { QStringLiteral("name"), raw.value(QStringLiteral("name")) }, + { QStringLiteral("symbol"), QString() }, + { QStringLiteral("type"), kind }, + { QStringLiteral("definitionId"), id }, + { QStringLiteral("definitionHex"), idHex }, + { QStringLiteral("metadataId"), metadataId }, + { QStringLiteral("source"), QStringLiteral("network") }, + }; + + if (fungible) { + const QString authorityMode = authority.isEmpty() + ? QStringLiteral("fixed") + : authority == id ? QStringLiteral("self") : QStringLiteral("external"); + const QString supply = raw.value(QStringLiteral("totalSupplyRaw")).toString(); + record.insert(QStringLiteral("rawSupply"), supply); + record.insert(QStringLiteral("displaySupply"), supply); + record.insert(QStringLiteral("inferredDecimals"), QString()); + record.insert(QStringLiteral("authorityMode"), authorityMode); + record.insert(QStringLiteral("authority"), authority); + record.insert(QStringLiteral("authorityLabel"), QString()); + record.insert(QStringLiteral("printableCopies"), QVariant()); + record.insert(QStringLiteral("masterHolding"), QVariant()); + record.insert(QStringLiteral("instruction"), metadataId.isEmpty() + ? QStringLiteral("createFungible") + : QStringLiteral("createFungibleWithMetadata")); + } else { + const QString printableSupply = + raw.value(QStringLiteral("printableSupplyRaw")).toString(); + record.insert(QStringLiteral("rawSupply"), QVariant()); + record.insert(QStringLiteral("displaySupply"), QVariant()); + record.insert(QStringLiteral("inferredDecimals"), QVariant()); + record.insert(QStringLiteral("authorityMode"), QStringLiteral("masterHolding")); + record.insert(QStringLiteral("authority"), QVariant()); + record.insert(QStringLiteral("authorityLabel"), QString()); + record.insert(QStringLiteral("printableCopies"), printableSupply); + record.insert(QStringLiteral("instruction"), QStringLiteral("createNonFungible")); + } + + QVariantMap normalizedDefinition { + { QStringLiteral("id"), id }, + { QStringLiteral("hex"), idHex }, + { QStringLiteral("name"), raw.value(QStringLiteral("name")) }, + { QStringLiteral("type"), kind }, + { QStringLiteral("metadataId"), metadataId }, + }; + if (fungible) { + normalizedDefinition.insert(QStringLiteral("totalSupplyRaw"), + raw.value(QStringLiteral("totalSupplyRaw"))); + normalizedDefinition.insert(QStringLiteral("mintAuthority"), authority); + } else { + normalizedDefinition.insert(QStringLiteral("printableSupply"), + raw.value(QStringLiteral("printableSupplyRaw"))); + } + record.insert(QStringLiteral("definition"), normalizedDefinition); + + if (!metadataId.isEmpty()) { + const QVariantMap metadataResult = m_logos->token_module.inspectMetadata(metadataId); + if (succeeded(metadataResult)) { + const QVariantMap rawMetadata = metadataResult.value(QStringLiteral("metadata")).toMap(); + const QString standard = displayMetadataStandard( + rawMetadata.value(QStringLiteral("standard")).toString()); + QVariantMap metadata { + { QStringLiteral("id"), rawMetadata.value(QStringLiteral("accountId")) }, + { QStringLiteral("standard"), standard }, + { QStringLiteral("uri"), rawMetadata.value(QStringLiteral("uri")) }, + { QStringLiteral("creators"), rawMetadata.value(QStringLiteral("creators")) }, + }; + record.insert(QStringLiteral("metadata"), metadata); + record.insert(QStringLiteral("metadataStandard"), standard); + record.insert(QStringLiteral("metadataUri"), + rawMetadata.value(QStringLiteral("uri"))); + record.insert(QStringLiteral("creators"), + rawMetadata.value(QStringLiteral("creators"))); + } + } + + QVariantList normalizedHoldings; + const QVariantList holdings = holdingsByDefinition.value(definition.key()); + for (const QVariant& holdingValue : holdings) + normalizedHoldings.append(normalizeHolding(holdingValue.toMap())); + record.insert(QStringLiteral("holdings"), normalizedHoldings); + + if (!normalizedHoldings.isEmpty()) { + const QVariantMap firstHolding = normalizedHoldings.first().toMap(); + record.insert(QStringLiteral("holding"), firstHolding); + record.insert(QStringLiteral("holdingId"), firstHolding.value(QStringLiteral("id"))); + if (!fungible && firstHolding.value(QStringLiteral("role")).toString() + == QStringLiteral("nftMaster")) { + record.insert(QStringLiteral("masterHolding"), + firstHolding.value(QStringLiteral("id"))); + } + } else { + record.insert(QStringLiteral("holdingId"), QString()); + } + + records.append(record); + } + + return records; +} + +QVariantMap TokenUiBackend::createFungible(QString definitionTargetId, + QString holdingTargetId, QString name, + QString totalSupplyRaw, QString mintAuthority) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.createFungible( + definitionTargetId, holdingTargetId, name, + QVariant::fromValue(totalSupplyRaw), mintAuthority)); +} + +QVariantMap TokenUiBackend::createFungibleWithMetadata( + QString definitionTargetId, QString holdingTargetId, QString metadataTargetId, + QString name, QString totalSupplyRaw, QString mintAuthority, + QString metadataStandard, QString uri, QString creators) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.createFungibleWithMetadata( + definitionTargetId, holdingTargetId, metadataTargetId, name, + QVariant::fromValue(totalSupplyRaw), mintAuthority, metadataStandard, uri, + creators)); +} + +QVariantMap TokenUiBackend::createNonFungible( + QString definitionTargetId, QString masterHoldingTargetId, + QString metadataTargetId, QString name, QString printableSupplyRaw, + QString metadataStandard, QString uri, QString creators) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.createNonFungible( + definitionTargetId, masterHoldingTargetId, metadataTargetId, name, + QVariant::fromValue(printableSupplyRaw), metadataStandard, uri, creators)); +} + +QVariantMap TokenUiBackend::initializeHolding(QString definitionId, QString holdingTargetId) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit( + m_logos->token_module.initializeHolding(definitionId, holdingTargetId)); +} + +QVariantMap TokenUiBackend::transfer(QString senderHoldingId, QString recipientHoldingId, + QString amountRaw) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.transfer( + senderHoldingId, recipientHoldingId, QVariant::fromValue(amountRaw))); +} + +QVariantMap TokenUiBackend::burn(QString definitionId, QString holdingId, QString amountRaw) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.burn( + definitionId, holdingId, QVariant::fromValue(amountRaw))); +} + +QVariantMap TokenUiBackend::mint(QString definitionId, QString holdingId, QString amountRaw) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.mint( + definitionId, holdingId, QVariant::fromValue(amountRaw))); +} + +QVariantMap TokenUiBackend::mintWithAuthority(QString definitionId, QString holdingId, + QString authorityId, QString amountRaw) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.mintWithAuthority( + definitionId, holdingId, authorityId, QVariant::fromValue(amountRaw))); +} + +QVariantMap TokenUiBackend::setAuthority(QString definitionId, QString newAuthority) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit( + m_logos->token_module.setAuthority(definitionId, newAuthority)); +} + +QVariantMap TokenUiBackend::setAuthorityWithAuthority(QString definitionId, + QString authorityId, + QString newAuthority) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit(m_logos->token_module.setAuthorityWithAuthority( + definitionId, authorityId, newAuthority)); +} + +QVariantMap TokenUiBackend::printNft(QString masterHoldingId, + QString printedHoldingTargetId) +{ + if (!m_walletController->state().isWalletOpen) + return walletUnavailable(); + return refreshAfterSubmit( + m_logos->token_module.printNft(masterHoldingId, printedHoldingTargetId)); +} diff --git a/apps/token/src/TokenUiBackend.h b/apps/token/src/TokenUiBackend.h index 4d26303..8d82765 100644 --- a/apps/token/src/TokenUiBackend.h +++ b/apps/token/src/TokenUiBackend.h @@ -1,26 +1,91 @@ #ifndef TOKEN_UI_BACKEND_H #define TOKEN_UI_BACKEND_H +#include + #include +#include +#include +#include #include "rep_TokenUiBackend_source.h" -#include "AccountModel.h" -#include "WalletBackendLogic.h" +#include "WalletAccountModel.h" class LogosAPI; +struct LogosModules; +class LogosWalletProvider; +class WalletController; -// Per-app wallet backend. All wallet behaviour lives in the shared -// WalletBackendLogic CRTP base (apps/common/wallet-ui), parameterised on the -// QtRO source generated from TokenUiBackend.rep. This class only adds the -// module identity and the accountModel property exposed to QML. -class TokenUiBackend : public WalletBackendLogic { +// Source-side implementation of the Token UI view contract. Wallet lifecycle +// stays in the reusable shared wallet classes; token reads and submissions are +// forwarded to the token_module core module. +class TokenUiBackend : public TokenUiBackendSimpleSource { Q_OBJECT - Q_PROPERTY(AccountModel* accountModel READ accountModel CONSTANT) + Q_PROPERTY(WalletAccountModel* accountModel READ accountModel CONSTANT) public: - explicit TokenUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr) - : WalletBackendLogic(logosAPI, parent, "token_ui", "TokenUI") {} + explicit TokenUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr); + ~TokenUiBackend() override; + + WalletAccountModel* accountModel() const; + +public slots: + QString createAccountPublic() override; + QString createAccountPrivate() override; + void refreshAccounts() override; + void refreshBalances() override; + QString getBalance(QString accountIdHex, bool isPublic) override; + QString createNewDefault(QString password) override; + QString createNew(QString configPath, QString storagePath, QString password) override; + bool openExisting() override; + void disconnectWallet() override; + + QVariantMap tokenProgramInfo() override; + QVariantMap inspectDefinition(QString definitionId) override; + QVariantMap inspectHolding(QString holdingId) override; + QVariantMap inspectMetadata(QString metadataId) override; + QVariantMap walletTokenAccounts() override; + QVariantList walletDefinitions() override; + + QVariantMap createFungible(QString definitionTargetId, QString holdingTargetId, + QString name, QString totalSupplyRaw, + QString mintAuthority) override; + QVariantMap createFungibleWithMetadata(QString definitionTargetId, + QString holdingTargetId, + QString metadataTargetId, QString name, + QString totalSupplyRaw, + QString mintAuthority, + QString metadataStandard, QString uri, + QString creators) override; + QVariantMap createNonFungible(QString definitionTargetId, + QString masterHoldingTargetId, + QString metadataTargetId, QString name, + QString printableSupplyRaw, + QString metadataStandard, QString uri, + QString creators) override; + + QVariantMap initializeHolding(QString definitionId, QString holdingTargetId) override; + QVariantMap transfer(QString senderHoldingId, QString recipientHoldingId, + QString amountRaw) override; + QVariantMap burn(QString definitionId, QString holdingId, QString amountRaw) override; + QVariantMap mint(QString definitionId, QString holdingId, QString amountRaw) override; + QVariantMap mintWithAuthority(QString definitionId, QString holdingId, + QString authorityId, QString amountRaw) override; + QVariantMap setAuthority(QString definitionId, QString newAuthority) override; + QVariantMap setAuthorityWithAuthority(QString definitionId, QString authorityId, + QString newAuthority) override; + QVariantMap printNft(QString masterHoldingId, QString printedHoldingTargetId) override; + +private: + void syncWalletState(); + QVariantMap walletUnavailable() const; + QVariantMap refreshAfterSubmit(QVariantMap result); + + LogosAPI* m_logosAPI; + std::unique_ptr m_logos; + std::unique_ptr m_wallet; + std::unique_ptr m_walletController; }; #endif // TOKEN_UI_BACKEND_H diff --git a/apps/token/src/TokenUiBackend.rep b/apps/token/src/TokenUiBackend.rep index bbad254..a38d02f 100644 --- a/apps/token/src/TokenUiBackend.rep +++ b/apps/token/src/TokenUiBackend.rep @@ -37,10 +37,29 @@ class TokenUiBackend // Basecamp, does not close the wallet other apps share. SLOT(void disconnectWallet()) - // Settings. Rewrites the wallet config's sequencer_addr and re-opens the - // wallet so the new network takes effect immediately. - SLOT(bool changeSequencerAddr(QString url)) + // Token Program reads. walletDefinitions() joins the module's account + // discovery and inspect calls into the record shape consumed by QML. + SLOT(QVariantMap tokenProgramInfo()) + SLOT(QVariantMap inspectDefinition(QString definitionId)) + SLOT(QVariantMap inspectHolding(QString holdingId)) + SLOT(QVariantMap inspectMetadata(QString metadataId)) + SLOT(QVariantMap walletTokenAccounts()) + SLOT(QVariantList walletDefinitions()) - // Misc - SLOT(void copyToClipboard(QString text)) + // Token Program creation surface. Raw amounts remain decimal strings so + // QML never narrows u128 values through a JavaScript number. + SLOT(QVariantMap createFungible(QString definitionTargetId, QString holdingTargetId, QString name, QString totalSupplyRaw, QString mintAuthority)) + SLOT(QVariantMap createFungibleWithMetadata(QString definitionTargetId, QString holdingTargetId, QString metadataTargetId, QString name, QString totalSupplyRaw, QString mintAuthority, QString metadataStandard, QString uri, QString creators)) + SLOT(QVariantMap createNonFungible(QString definitionTargetId, QString masterHoldingTargetId, QString metadataTargetId, QString name, QString printableSupplyRaw, QString metadataStandard, QString uri, QString creators)) + + // Token Program holding and authority operations are exposed through the + // same adapter for future Manage actions and Basecamp consumers. + SLOT(QVariantMap initializeHolding(QString definitionId, QString holdingTargetId)) + SLOT(QVariantMap transfer(QString senderHoldingId, QString recipientHoldingId, QString amountRaw)) + SLOT(QVariantMap burn(QString definitionId, QString holdingId, QString amountRaw)) + SLOT(QVariantMap mint(QString definitionId, QString holdingId, QString amountRaw)) + SLOT(QVariantMap mintWithAuthority(QString definitionId, QString holdingId, QString authorityId, QString amountRaw)) + SLOT(QVariantMap setAuthority(QString definitionId, QString newAuthority)) + SLOT(QVariantMap setAuthorityWithAuthority(QString definitionId, QString authorityId, QString newAuthority)) + SLOT(QVariantMap printNft(QString masterHoldingId, QString printedHoldingTargetId)) } diff --git a/flake.nix b/flake.nix index ce07db0..e1ab704 100644 --- a/flake.nix +++ b/flake.nix @@ -143,15 +143,42 @@ # built QML module the same way. Keep in sync with apps/amm/flake.nix. preConfigure = '' cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${./apps/shared/wallet}") + cmakeFlagsArray+=("-DLOGOS_WALLET_GENERATED_DIR=$PWD/generated_code/include") ''; postInstall = '' - test -f ${./apps/amm/qml}/Logos/Wallet/qmldir - - walletQmlDir="shared-wallet/qml/Logos/Wallet" - if [ ! -d "$walletQmlDir" ]; then + walletQmlDescriptor="$(find "$PWD" -type f -path '*/shared-wallet/qml/Logos/Wallet/qmldir' -print -quit)" + if [ -z "$walletQmlDescriptor" ]; then echo "Built Logos.Wallet QML module not found" exit 1 fi + walletQmlDir="$(dirname "$walletQmlDescriptor")" + walletQmlInstallDir="$out/lib/Logos/Wallet" + mkdir -p "$walletQmlInstallDir" + cp -r "$walletQmlDir/." "$walletQmlInstallDir/" + test -f "$walletQmlInstallDir/qmldir" + ''; + }; + + # Token QML UI (apps/token). This UI consumes the token_module core + # module through its generated Logos SDK and the reusable shared wallet + # access/QML module. Keep the output separate so Basecamp can install or + # run `token-ui` independently from the AMM UI. + tokenAppOutputs = logos-module-builder.lib.mkLogosQmlModule { + src = ./apps/token; + configFile = ./apps/token/metadata.json; + flakeInputs = inputs // { token_module = tokenModuleOutputs; }; + externalLibInputs = { }; + preConfigure = '' + cmakeFlagsArray+=("-DLOGOS_WALLET_SOURCE_DIR=${./apps/shared/wallet}") + cmakeFlagsArray+=("-DLOGOS_WALLET_GENERATED_DIR=$PWD/generated_code/include") + ''; + postInstall = '' + walletQmlDescriptor="$(find "$PWD" -type f -path '*/shared-wallet/qml/Logos/Wallet/qmldir' -print -quit)" + if [ -z "$walletQmlDescriptor" ]; then + echo "Built Logos.Wallet QML module not found" + exit 1 + fi + walletQmlDir="$(dirname "$walletQmlDescriptor")" walletQmlInstallDir="$out/lib/Logos/Wallet" mkdir -p "$walletQmlInstallDir" cp -r "$walletQmlDir/." "$walletQmlInstallDir/" @@ -167,6 +194,8 @@ # for either attribute set. appApps = appOutputs.apps or { }; appPkgs = appOutputs.packages or { }; + tokenAppApps = tokenAppOutputs.apps or { }; + tokenAppPkgs = tokenAppOutputs.packages or { }; # AMM core module (modules/amm): the AMM business logic as a headless # `core` Logos module. It links the amm_ffi crate (the transport- @@ -224,16 +253,29 @@ (builtins.removeAttrs attrs [ "default" ]) // (if attrs ? default then { amm-ui = wrapWithDyld system attrs.default; } else { }) ) appApps; + renamedTokenApps = builtins.mapAttrs ( + system: attrs: + (builtins.removeAttrs attrs [ "default" ]) // (if attrs ? default then { token-ui = attrs.default; } else { }) + ) tokenAppApps; + + mergedApps = builtins.mapAttrs ( + system: attrs: + attrs // (renamedTokenApps.${system} or { }) + ) renamedApps; + mergedPackages = builtins.mapAttrs ( system: cratePkgs: let appSysPkgs = appPkgs.${system} or { }; + tokenAppSysPkgs = tokenAppPkgs.${system} or { }; ammModSysPkgs = ammModulePkgs.${system} or { }; tokenModSysPkgs = tokenModulePkgs.${system} or { }; in (builtins.removeAttrs cratePkgs [ "default" ]) // (builtins.removeAttrs appSysPkgs [ "default" ]) // (if appSysPkgs ? default then { amm-ui = appSysPkgs.default; } else { }) + // (builtins.removeAttrs tokenAppSysPkgs [ "default" ]) + // (if tokenAppSysPkgs ? default then { token-ui = tokenAppSysPkgs.default; } else { }) // (builtins.removeAttrs ammModSysPkgs [ "default" ]) // (if ammModSysPkgs ? default then { amm-module = ammModSysPkgs.default; } else { }) // (builtins.removeAttrs tokenModSysPkgs [ "default" ]) @@ -242,7 +284,7 @@ in (builtins.removeAttrs appOutputs [ "apps" "packages" ]) // { - apps = renamedApps; + apps = mergedApps; packages = mergedPackages; }; }