mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
feat(token-ui): connect Basecamp UI to token module
This commit is contained in:
@@ -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/<app>/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 <pluginDir>/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
|
||||
(`<standalone>/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 `<app>/lib/Logos/Wallet` via `postInstall`; used as `import Logos.Wallet` |
|
||||
| `src/AccountModel.{h,cpp}` | `QAbstractListModel` of wallet accounts, exposed to QML via `logos.model("<app>_ui", "accountModel")` | Overlaid into `<app>/src/` at build |
|
||||
| `src/WalletBackendLogic.h` | All wallet behaviour (open/adopt, account create, balances, sequencer settings, reachability) as a CRTP base | Overlaid into `<app>/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
|
||||
`<App>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<Base>` 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<TokenUiBackendSimpleSource> {
|
||||
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 `<App>UiBackend.rep`
|
||||
in lockstep (the surfaces must stay identical) and the CMake `SOURCES` lists if
|
||||
you add files.
|
||||
@@ -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("<app>_ui")) and account model
|
||||
// are passed in by the host app via the `backend`/`accountModel` properties.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Backend replica (logos.module("<app>_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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" fill="#000"/><path d="M4 20c0-3.866 3.582-6 8-6s8 2.134 8 6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 253 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M15 5l-7 7 7 7"/></svg>
|
||||
|
Before Width: | Height: | Size: 206 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" height="17" viewBox="0 0 16 17" width="16" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m6.76413 10.1901 5.56067-5.41122c.382-.37171 1-.37297 1.3863.00299.3837.37336.3857.97674.0031 1.34906l-6.25845 6.09017c-.18993.1849-.43822.2781-.68737.2789-.25655-.0019-.50554-.0937-.69254-.2756l-2.79161-2.7166c-.38013-.36991-.37994-.96985.00641-1.34581.38367-.37336 1.00799-.37115 1.38299-.00623z" fill="#000" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 463 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="#000"><path d="m4.16634 7c.27614 0 .5-.22386.5-.5s-.22386-.5-.5-.5h-.16667c-1.47275 0-2.66666 1.19391-2.66666 2.66667v3.33333c0 1.4728 1.19391 2.6667 2.66666 2.6667h3.33334c1.47276 0 2.66666-1.1939 2.66666-2.6667v-.1667c0-.2761-.22385-.5-.5-.5-.27614 0-.5.2239-.5.5v.1667c0 .9205-.74619 1.6667-1.66666 1.6667h-3.33334c-.92047 0-1.66666-.7462-1.66666-1.6667v-3.33333c0-.92048.74619-1.66667 1.66666-1.66667z"/><path clip-rule="evenodd" d="m5.99967 4c0-1.47276 1.19391-2.66666 2.66667-2.66666h3.33336c1.4727 0 2.6666 1.1939 2.6666 2.66666v3.33334c0 1.47276-1.1939 2.66666-2.6666 2.66666h-3.33336c-1.47276 0-2.66667-1.1939-2.66667-2.66666zm2.66667-1.66666h3.33336c.9204 0 1.6666.74619 1.6666 1.66666v3.33334c0 .92047-.7462 1.66666-1.6666 1.66666h-3.33336c-.92047 0-1.66667-.74619-1.66667-1.66666v-3.33334c0-.92047.7462-1.66666 1.66667-1.66666z" fill-rule="evenodd"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 977 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M12 3v9"/><path d="M7.05 7.05a7 7 0 1 0 9.9 0"/></svg>
|
||||
|
Before Width: | Height: | Size: 237 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 845 B |
@@ -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
|
||||
@@ -1,79 +0,0 @@
|
||||
#include "AccountModel.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
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<int, QByteArray> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
// 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("<app>_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<int, QByteArray> 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<AccountEntry> m_entries;
|
||||
};
|
||||
@@ -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 <App>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<Base> 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<FooUiBackendSimpleSource> {
|
||||
// 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 <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
|
||||
#include "AccountModel.h"
|
||||
|
||||
// Base is the generated <App>UiBackendSimpleSource. We inherit it so we can
|
||||
// access its protected PROP setters and override its pure-virtual .rep slots.
|
||||
template <class Base>
|
||||
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<quint64>(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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+24
-26
@@ -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.
|
||||
|
||||
Generated
+4
-4
@@ -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": []
|
||||
|
||||
+28
-61
@@ -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 <pluginDir>/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"
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module Logos.Wallet
|
||||
optional plugin logos_wallet_qmlplugin ../../../Logos/Wallet
|
||||
classname Logos_WalletPlugin
|
||||
prefer :/qt/qml/Logos/Wallet/
|
||||
depends QtQuick
|
||||
+14
-8
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1358
-1049
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 <QMap>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
#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<LogosModules>(m_logosAPI)),
|
||||
m_wallet(std::make_unique<LogosWalletProvider>(m_logosAPI)),
|
||||
m_walletController(std::make_unique<WalletController>(
|
||||
*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<QString, QString> definitionIds;
|
||||
QMap<QString, QVariantList> 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));
|
||||
}
|
||||
|
||||
@@ -1,26 +1,91 @@
|
||||
#ifndef TOKEN_UI_BACKEND_H
|
||||
#define TOKEN_UI_BACKEND_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
#include <QVariantList>
|
||||
|
||||
#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<TokenUiBackendSimpleSource> {
|
||||
// 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<LogosModules> m_logos;
|
||||
std::unique_ptr<LogosWalletProvider> m_wallet;
|
||||
std::unique_ptr<WalletController> m_walletController;
|
||||
};
|
||||
|
||||
#endif // TOKEN_UI_BACKEND_H
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user