feat(token): add token definition app

This commit is contained in:
Ricardo Guilherme Schmidt
2026-08-20 13:30:33 +02:00
committed by r4bbit
parent 741e72add9
commit b70eeb40b1
35 changed files with 32459 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
# 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.
@@ -0,0 +1,499 @@
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) })
}
}
}
@@ -0,0 +1,84 @@
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
}
}
}
}
@@ -0,0 +1,102 @@
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()
}
}
}
}
}
@@ -0,0 +1,201 @@
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()
}
}
}
}
@@ -0,0 +1,39 @@
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()
}
}
@@ -0,0 +1,25 @@
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
}
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 253 B

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 206 B

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 463 B

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 977 B

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 237 B

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 845 B

@@ -0,0 +1,4 @@
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
@@ -0,0 +1,79 @@
#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;
}
}
}
+48
View File
@@ -0,0 +1,48 @@
#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;
};
@@ -0,0 +1,453 @@
#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
+31
View File
@@ -0,0 +1,31 @@
cmake_minimum_required(VERSION 3.14)
project(TokenUiPlugin LANGUAGES CXX)
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.
logos_module(
NAME token_ui
REP_FILE src/TokenUiBackend.rep
SOURCES
src/TokenUiPluginInterface.h
src/TokenUiPlugin.h
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
)
+88
View File
@@ -0,0 +1,88 @@
# Token UI
A QML UI application for the token program.
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.
## Token-definition prototype
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:
- Fungible definitions: raw `u128` supply; fixed (`None`), self, or external
mint authority; metadata omitted or linked.
- Non-fungible definitions: printable supply plus required metadata, with an
initial master holding that controls printing.
- Metadata: `Simple` or `Expanded` standard, URI, and creators string. The
program initializes `primary_sale_date` to `0`; it has no creation input for
decimals, symbol, description, image, royalties, collection, or mutable
metadata.
- 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.
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.
## 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.
**Onboarding is non-invasive.** The app opens straight to the first screen; the
navbar shows **Connect** (opens a password-only modal) or **Connected** + the
account selector. There is no path picking — the wallet uses LEZ's canonical
home, `~/.lee/wallet/` (override with `LEE_WALLET_HOME_DIR`, the same var LEZ
honors), and its config (`wallet_config.json`) self-initializes.
Account/keystore sharing follows the runtime:
- **Standalone** (`nix run .`): own core-module instance, but the canonical
`~/.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.
## Setup
This project requires Nix with experimental features enabled. If you haven't already, enable them permanently:
```bash
mkdir -p ~/.config/nix && echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
```
## Running the UI
Start the UI with:
```bash
nix run .
```
This builds and runs the application in development mode.
## Updating Dependencies
To update the pinned versions of dependencies in `flake.lock`:
```bash
nix flake update
```
+26873
View File
File diff suppressed because it is too large Load Diff
+71
View File
@@ -0,0 +1,71 @@
{
description = "Logos Token QML UI create and manage tokens on the LEZ token program";
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";
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});
};
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+25
View File
@@ -0,0 +1,25 @@
{
"name": "token_ui",
"version": "0.1.0",
"type": "ui_qml",
"category": "token",
"description": "UI module for the token program",
"main": "token_ui_plugin",
"view": "qml/Main.qml",
"icon": "icons/token.png",
"dependencies": ["logos_execution_zone"],
"nix": {
"packages": {
"build": [],
"runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp"]
},
"external_libraries": [],
"cmake": {
"find_packages": [],
"extra_sources": [],
"extra_include_dirs": [],
"extra_link_libraries": []
}
}
}
+101
View File
@@ -0,0 +1,101 @@
import QtQuick 2.15
import Logos.Theme
import "pages"
import "state"
Item {
id: root
// Backend replica + account model, bridged from the C++ backend.
readonly property var backend: logos.module("token_ui")
readonly property var accountModel: logos.model("token_ui", "accountModel")
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
}
Connections {
target: logos
function onViewModuleReadyChanged(moduleName, isReady) {
if (moduleName === "token_ui")
root.ready = isReady && root.backend !== null;
}
}
Component.onCompleted: {
root.ready = root.backend !== null && logos.isViewModuleReady("token_ui");
}
// Connectivity banner: shown when a wallet is open but its configured
// sequencer doesn't answer reachability probes (so transactions will fail).
Rectangle {
id: connectionBanner
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
z: 101
readonly property bool show: root.ready && root.backend && root.backend.isWalletOpen && root.backend.sequencerAddr.length > 0 && !root.backend.sequencerReachable
height: show ? 32 : 0
visible: height > 0
clip: true
color: Theme.palette.warning
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
Text {
anchors.centerIn: parent
width: parent.width - 40
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideMiddle
font.pixelSize: 12
font.weight: Font.Medium
color: Theme.palette.background
text: qsTr("Unable to connect to network")
}
}
// The app is always usable; the wallet is opt-in via the navbar "Connect"
// control. Prototype views render immediately and stay local-only.
NavBar {
id: navbar
anchors.top: connectionBanner.bottom
anchors.left: parent.left
anchors.right: parent.right
z: 100
backend: root.ready ? root.backend : null
accountModel: root.accountModel
}
Item {
anchors.top: navbar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
CreatePage {
anchors.fill: parent
visible: navbar.currentIndex === 0
store: tokenPrototypeStore
}
ManagePage {
anchors.fill: parent
visible: navbar.currentIndex === 1
store: tokenPrototypeStore
}
}
}
+137
View File
@@ -0,0 +1,137 @@
pragma ComponentBehavior: Bound
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"
// Self-contained navigation bar — styling is independent of any view's theme.
// Use currentIndex to read the active tab; tabChanged(index) fires on selection.
Item {
id: root
property int currentIndex: 0
readonly property var tabs: [qsTr("Create"), qsTr("Inspect")]
// Wallet wiring, passed down from Main.qml.
property var backend: null
property var accountModel: null
// Address of the account currently selected in the header control.
readonly property string selectedAddress: accountControl.selectedAddress
signal tabChanged(int index)
implicitHeight: 56
Rectangle {
anchors.fill: parent
color: Theme.palette.background
// Bottom separator
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.palette.borderSecondary
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 20
anchors.rightMargin: 20
spacing: 4
// App identity
Text {
text: qsTr("Logos Token")
color: Theme.palette.text
font.pixelSize: 17
font.weight: Font.Bold
}
Item {
Layout.fillWidth: true
}
// Tab pills
Row {
spacing: 4
Repeater {
model: root.tabs
delegate: Rectangle {
id: tabPill
required property int index
required property var modelData
readonly property int tabIndex: index
readonly property bool active: root.currentIndex === tabIndex
height: 36
width: tabLabel.implicitWidth + 28
radius: 18
color: active ? Theme.palette.backgroundSecondary : "transparent"
border.color: activeFocus ? Theme.palette.overlayOrange : "transparent"
border.width: activeFocus ? 1 : 0
activeFocusOnTab: true
Accessible.name: qsTr("Open %1").arg(tabPill.modelData)
Accessible.role: Accessible.Button
Accessible.onPressAction: tabPill.activate()
function activate() {
root.currentIndex = tabPill.tabIndex;
root.tabChanged(tabPill.tabIndex);
}
Keys.onReturnPressed: tabPill.activate()
Keys.onSpacePressed: tabPill.activate()
Behavior on color {
ColorAnimation {
duration: 150
}
}
Text {
id: tabLabel
anchors.centerIn: parent
text: tabPill.modelData
color: tabPill.active ? Theme.palette.text : Theme.palette.textSecondary
font.pixelSize: 14
font.weight: tabPill.active ? Font.Medium : Font.Normal
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: tabPill.activate()
}
}
}
}
// Wallet / account control on the far right.
WalletControl {
id: accountControl
Layout.leftMargin: 12
backend: root.backend
accountModel: root.accountModel
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,673 @@
import QtQuick 2.15
QtObject {
id: root
readonly property string network: "testnet"
readonly property string tokenProgramId: "F8sGbDbjcxvJHpUQJcArEaY7EbLMVmqZgRm3fXPw3jb3"
readonly property var fixtureDefinitions: [
{
"id": "9JDLE5Qr8dXKBstucN5sZi5tCCYy7SnfCEKax77JZTd7",
"name": "Pebble",
"symbol": "",
"type": "fungible",
"definitionId": "9JDLE5Qr8dXKBstucN5sZi5tCCYy7SnfCEKax77JZTd7",
"definitionHex": "7b464ff9dd0d3bc07f7e2e0b0667ccd066d85ad12be4c79fc55687a863910aa6",
"holdingId": "DhKocL4KzaRbL25Dw3V8rDvTa6aNefxyCAb8F22Tyazn",
"metadataId": null,
"rawSupply": "7654321",
"displaySupply": "7,654,321",
"inferredDecimals": 0,
"authorityMode": "fixed",
"authority": null,
"metadataStandard": null,
"metadataUri": null,
"creators": null,
"description": null,
"source": "testnet",
"instruction": "new_fungible_definition",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "9JDLE5Qr8dXKBstucN5sZi5tCCYy7SnfCEKax77JZTd7",
"hex": "7b464ff9dd0d3bc07f7e2e0b0667ccd066d85ad12be4c79fc55687a863910aa6",
"name": "Pebble",
"type": "fungible",
"totalSupplyRaw": "7654321",
"mintAuthority": null,
"metadataId": null
},
"holding": {
"id": "DhKocL4KzaRbL25Dw3V8rDvTa6aNefxyCAb8F22Tyazn",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "6419754",
"displayBalance": "6,419,754"
},
"holdings": [
{
"id": "DhKocL4KzaRbL25Dw3V8rDvTa6aNefxyCAb8F22Tyazn",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "6419754",
"displayBalance": "6,419,754"
},
{
"id": "78Ynj85rpDFfAH2Tk1F296k6uJrU2h8mFoAsJg8sM3DZ",
"wallet": "single-asset",
"role": "recipient",
"rawBalance": "1234567",
"displayBalance": "1,234,567"
}
],
"metadata": null
},
{
"id": "5u7LShcFeBwFubWbD1N6jBYMGonEuj1sfi2A6j9D5UK3",
"name": "Aurora",
"symbol": "",
"type": "fungible",
"definitionId": "5u7LShcFeBwFubWbD1N6jBYMGonEuj1sfi2A6j9D5UK3",
"definitionHex": "48c81cf032e601ca367fc9816b957dbf5c0e4c11cf7008e8f4581ec1a67aab42",
"holdingId": "5rZuJSHTm2NggSBFbKuyZd6D3f7qWAhTB7YsgFZDYe8",
"metadataId": null,
"rawSupply": "98765432100",
"displaySupply": "98,765.4321",
"inferredDecimals": 6,
"authorityMode": "fixed",
"authority": null,
"metadataStandard": null,
"metadataUri": null,
"creators": null,
"description": null,
"source": "testnet",
"instruction": "new_fungible_definition",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "5u7LShcFeBwFubWbD1N6jBYMGonEuj1sfi2A6j9D5UK3",
"hex": "48c81cf032e601ca367fc9816b957dbf5c0e4c11cf7008e8f4581ec1a67aab42",
"name": "Aurora",
"type": "fungible",
"totalSupplyRaw": "98765432100",
"mintAuthority": null,
"metadataId": null
},
"holding": {
"id": "5rZuJSHTm2NggSBFbKuyZd6D3f7qWAhTB7YsgFZDYe8",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "90000000000",
"displayBalance": "90,000"
},
"holdings": [
{
"id": "5rZuJSHTm2NggSBFbKuyZd6D3f7qWAhTB7YsgFZDYe8",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "90000000000",
"displayBalance": "90,000"
},
{
"id": "HrC5H28wnK64dsx4xDkUUPipxDvcNFmB9FizJ4vDEHLR",
"wallet": "mixed-assets",
"role": "recipient",
"rawBalance": "8765432100",
"displayBalance": "8,765.4321"
}
],
"metadata": null
},
{
"id": "2TN8jTUxgxRZgATDGANqiNuALpAyUM3Lks6wiNYvSdDi",
"name": "Cobalt",
"symbol": "",
"type": "fungible",
"definitionId": "2TN8jTUxgxRZgATDGANqiNuALpAyUM3Lks6wiNYvSdDi",
"definitionHex": "159caef810ea545951b3bd913efe625ee45008c80865c330e72a72ed48b61649",
"holdingId": "7mKCcw4dtQW6LyxFazZpB7XUisfGLRNfxBcWEiJaBvgZ",
"metadataId": null,
"rawSupply": "9876543210000",
"displaySupply": "9,876,543.21",
"inferredDecimals": 6,
"authorityMode": "fixed",
"authority": null,
"metadataStandard": null,
"metadataUri": null,
"creators": null,
"description": null,
"source": "testnet",
"instruction": "new_fungible_definition",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "2TN8jTUxgxRZgATDGANqiNuALpAyUM3Lks6wiNYvSdDi",
"hex": "159caef810ea545951b3bd913efe625ee45008c80865c330e72a72ed48b61649",
"name": "Cobalt",
"type": "fungible",
"totalSupplyRaw": "9876543210000",
"mintAuthority": null,
"metadataId": null
},
"holding": {
"id": "7mKCcw4dtQW6LyxFazZpB7XUisfGLRNfxBcWEiJaBvgZ",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "9876543210000",
"displayBalance": "9,876,543.21"
},
"holdings": [
{
"id": "7mKCcw4dtQW6LyxFazZpB7XUisfGLRNfxBcWEiJaBvgZ",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "9876543210000",
"displayBalance": "9,876,543.21"
}
],
"metadata": null
},
{
"id": "8wRnGribscuUhJJFca4nXwR8iAYtrpUU1JXLqZNXMSou",
"name": "Meridian",
"symbol": "",
"type": "fungible",
"definitionId": "8wRnGribscuUhJJFca4nXwR8iAYtrpUU1JXLqZNXMSou",
"definitionHex": "75f33110b185717209e3955f228d4a4448801d0ce8ba438a4a268050eeff3f44",
"holdingId": "DNckKy9rUwohZS51ZYGxpw6Ke2WYusaBwqDecMzqCiqF",
"metadataId": null,
"rawSupply": "123456789012345678",
"displaySupply": "123,456,789.012345678",
"inferredDecimals": 9,
"authorityMode": "fixed",
"authority": null,
"metadataStandard": null,
"metadataUri": null,
"creators": null,
"description": null,
"source": "testnet",
"instruction": "new_fungible_definition",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "8wRnGribscuUhJJFca4nXwR8iAYtrpUU1JXLqZNXMSou",
"hex": "75f33110b185717209e3955f228d4a4448801d0ce8ba438a4a268050eeff3f44",
"name": "Meridian",
"type": "fungible",
"totalSupplyRaw": "123456789012345678",
"mintAuthority": null,
"metadataId": null
},
"holding": {
"id": "DNckKy9rUwohZS51ZYGxpw6Ke2WYusaBwqDecMzqCiqF",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "100000000000000000",
"displayBalance": "100,000,000"
},
"holdings": [
{
"id": "DNckKy9rUwohZS51ZYGxpw6Ke2WYusaBwqDecMzqCiqF",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "100000000000000000",
"displayBalance": "100,000,000"
},
{
"id": "4TkHNLiC6WQeFkeTmh6bAcLzYYhqbeJyRci92bXAcF8D",
"wallet": "mixed-assets",
"role": "recipient",
"rawBalance": "23456789012345678",
"displayBalance": "23,456,789.012345678"
}
],
"metadata": null
},
{
"id": "HwzCapv2fVYKhrdK68Q7QwdhfNkJDXrCGxqkW8e9UjDG",
"name": "Quartz",
"symbol": "",
"type": "fungible",
"definitionId": "HwzCapv2fVYKhrdK68Q7QwdhfNkJDXrCGxqkW8e9UjDG",
"definitionHex": "fbd107ca4bb66bc58f59ac2d32a759be3ee0fb453f8fecd1991c11837d9660c7",
"holdingId": "AtBFdo6vXRoDHwadHPHvMk84bW3fLtjy7YHF5UeS7Trx",
"metadataId": null,
"rawSupply": "12345678901234567890123456",
"displaySupply": "12,345,678.901234567890123456",
"inferredDecimals": 18,
"authorityMode": "fixed",
"authority": null,
"metadataStandard": null,
"metadataUri": null,
"creators": null,
"description": null,
"source": "testnet",
"instruction": "new_fungible_definition",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "HwzCapv2fVYKhrdK68Q7QwdhfNkJDXrCGxqkW8e9UjDG",
"hex": "fbd107ca4bb66bc58f59ac2d32a759be3ee0fb453f8fecd1991c11837d9660c7",
"name": "Quartz",
"type": "fungible",
"totalSupplyRaw": "12345678901234567890123456",
"mintAuthority": null,
"metadataId": null
},
"holding": {
"id": "AtBFdo6vXRoDHwadHPHvMk84bW3fLtjy7YHF5UeS7Trx",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "10000000000000000000000000",
"displayBalance": "10,000,000"
},
"holdings": [
{
"id": "AtBFdo6vXRoDHwadHPHvMk84bW3fLtjy7YHF5UeS7Trx",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "10000000000000000000000000",
"displayBalance": "10,000,000"
},
{
"id": "3JqMVkxAnSVuvetAppW5g3jKrFKv5otyhHNUkYw859p2",
"wallet": "mixed-assets",
"role": "recipient",
"rawBalance": "2345678901234567890123456",
"displayBalance": "2,345,678.901234567890123456"
}
],
"metadata": null
},
{
"id": "6juKZuiBbxwymyet38MnsvidbsomcQEYLR8jhaFYp7f4",
"name": "Devium",
"symbol": "",
"type": "fungible",
"definitionId": "6juKZuiBbxwymyet38MnsvidbsomcQEYLR8jhaFYp7f4",
"definitionHex": "5547fcb72644d95a385d313b887a96be41ff263bce6150b49fd87276839822bf",
"holdingId": "63JPusYpdEEtri5ZHcLqfeM9XdnNsbc3pVdwhFnZB6ep",
"metadataId": null,
"rawSupply": "1000000750000000000000000000000",
"displaySupply": "1,000,000,750,000",
"inferredDecimals": 18,
"authorityMode": "external",
"authority": "HLf2CQotnxpjsrG98xrtUb7qoQcXufU6ARtMAdcuP55c",
"authorityLabel": "Devium Authority",
"authorityHex": "f2c40429b1e77773dae8c4d498aa0ff02a71d187133dcd87d9403c1de787eaab",
"metadataStandard": null,
"metadataUri": null,
"creators": null,
"description": null,
"source": "testnet",
"instruction": "new_fungible_definition",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "6juKZuiBbxwymyet38MnsvidbsomcQEYLR8jhaFYp7f4",
"hex": "5547fcb72644d95a385d313b887a96be41ff263bce6150b49fd87276839822bf",
"name": "Devium",
"type": "fungible",
"totalSupplyRaw": "1000000750000000000000000000000",
"mintAuthority": "HLf2CQotnxpjsrG98xrtUb7qoQcXufU6ARtMAdcuP55c",
"metadataId": null
},
"holding": {
"id": "63JPusYpdEEtri5ZHcLqfeM9XdnNsbc3pVdwhFnZB6ep",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "1000000750000000000000000000000",
"displayBalance": "1,000,000,750,000"
},
"holdings": [
{
"id": "63JPusYpdEEtri5ZHcLqfeM9XdnNsbc3pVdwhFnZB6ep",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "1000000750000000000000000000000",
"displayBalance": "1,000,000,750,000"
}
],
"metadata": null
},
{
"id": "Hqvym6HLGdu3WXZendqNKLL2feRmwFJ46CrVYhVBw1ti",
"name": "Mint Condition",
"symbol": "",
"type": "fungible",
"definitionId": "Hqvym6HLGdu3WXZendqNKLL2feRmwFJ46CrVYhVBw1ti",
"definitionHex": "fa43e74a97d79c5f907ff3edabda5ad89bfbd3b0922572e675d4ad3c7b6029c7",
"holdingId": "HPYP64fjV6fRRk8rDMVFU4JXnh8g9SMpfFLMFVUBirVx",
"metadataId": null,
"rawSupply": "164803398874989484820",
"displaySupply": "164,803,398,874.98948482",
"inferredDecimals": 9,
"authorityMode": "renounced",
"authority": null,
"initialAuthority": "Hqvym6HLGdu3WXZendqNKLL2feRmwFJ46CrVYhVBw1ti",
"metadataStandard": null,
"metadataUri": null,
"creators": null,
"description": null,
"source": "testnet",
"instruction": "new_fungible_definition",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "Hqvym6HLGdu3WXZendqNKLL2feRmwFJ46CrVYhVBw1ti",
"hex": "fa43e74a97d79c5f907ff3edabda5ad89bfbd3b0922572e675d4ad3c7b6029c7",
"name": "Mint Condition",
"type": "fungible",
"totalSupplyRaw": "164803398874989484820",
"mintAuthority": null,
"metadataId": null
},
"holding": {
"id": "HPYP64fjV6fRRk8rDMVFU4JXnh8g9SMpfFLMFVUBirVx",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "164803398874989484820",
"displayBalance": "164,803,398,874.98948482"
},
"holdings": [
{
"id": "HPYP64fjV6fRRk8rDMVFU4JXnh8g9SMpfFLMFVUBirVx",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "164803398874989484820",
"displayBalance": "164,803,398,874.98948482"
}
],
"metadata": null
},
{
"id": "6L9bqMV8YiRpTNfqrZaZAyAFwqyNKHG2iQehRqANtVxk",
"name": "Oops! All Metadata",
"symbol": "",
"type": "fungible",
"definitionId": "6L9bqMV8YiRpTNfqrZaZAyAFwqyNKHG2iQehRqANtVxk",
"definitionHex": "4f3231d8a01e1d79f163bc27fce0c860a4a2f6890280e9d135eafbde0d68ed79",
"holdingId": "B8juLRsBJLSAaFtFhwx3K3PpcxZ4aKkxnkh6LeFzdjBy",
"metadataId": "AHUVjXRgedSGexq9ebwHZ2yfoXed2BKfMm2MQAcxE8WZ",
"rawSupply": "424218967453",
"displaySupply": "424,218.967453",
"inferredDecimals": 6,
"authorityMode": "fixed",
"authority": null,
"metadataStandard": "Simple",
"metadataUri": "data:application/json;base64,eyJuYW1lIjoiT29wcyEgQWxsIE1ldGFkYXRhIiwiZGVzY3JpcHRpb24iOiJBIHRva2VuIHdpdGggbW9yZSBtZXRhZGF0YSB0aGFuIHNlbnNlLiIsIm1vZGUiOiJmaXhlZCIsInN0YW5kYXJkIjoiU2ltcGxlIn0=",
"creators": "Department of Redundant Metadata",
"description": "A token with more metadata than sense.",
"source": "testnet",
"instruction": "new_definition_with_metadata",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "6L9bqMV8YiRpTNfqrZaZAyAFwqyNKHG2iQehRqANtVxk",
"hex": "4f3231d8a01e1d79f163bc27fce0c860a4a2f6890280e9d135eafbde0d68ed79",
"name": "Oops! All Metadata",
"type": "fungible",
"totalSupplyRaw": "424218967453",
"mintAuthority": null,
"metadataId": "AHUVjXRgedSGexq9ebwHZ2yfoXed2BKfMm2MQAcxE8WZ"
},
"holding": {
"id": "B8juLRsBJLSAaFtFhwx3K3PpcxZ4aKkxnkh6LeFzdjBy",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "424118967453",
"displayBalance": "424,118.967453"
},
"holdings": [
{
"id": "B8juLRsBJLSAaFtFhwx3K3PpcxZ4aKkxnkh6LeFzdjBy",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "424118967453",
"displayBalance": "424,118.967453"
},
{
"id": "Dxd4WrxvZHmbCCVkFB7dzh6UYcKTc6GLegPS1XeAqowr",
"wallet": "all-assets",
"role": "recipient",
"rawBalance": "100000000",
"displayBalance": "100"
}
],
"metadata": {
"id": "AHUVjXRgedSGexq9ebwHZ2yfoXed2BKfMm2MQAcxE8WZ",
"standard": "Simple",
"uri": "data:application/json;base64,eyJuYW1lIjoiT29wcyEgQWxsIE1ldGFkYXRhIiwiZGVzY3JpcHRpb24iOiJBIHRva2VuIHdpdGggbW9yZSBtZXRhZGF0YSB0aGFuIHNlbnNlLiIsIm1vZGUiOiJmaXhlZCIsInN0YW5kYXJkIjoiU2ltcGxlIn0=",
"creators": "Department of Redundant Metadata",
"description": "A token with more metadata than sense."
}
},
{
"id": "Hqfz9dfGeMgWbL9rhUnMPLgdQZUMczjNjUWCTRozkkk7",
"name": "Pocket Lint Deluxe",
"symbol": "",
"type": "fungible",
"definitionId": "Hqfz9dfGeMgWbL9rhUnMPLgdQZUMczjNjUWCTRozkkk7",
"definitionHex": "fa32f354408857006f8ea396b0419823bd04436eadb2d273d2618a46b4793ed8",
"holdingId": "7aDup3sUWkFF2RZnCXyo78BEvaKT3Wf4dWrPTqaMeRXj",
"metadataId": "GciAureLmwkCKuELgo2YzF1dYhh8Jcu7cMciquuY7no5",
"rawSupply": "10000000000000000000000000",
"displaySupply": "10,000,000",
"inferredDecimals": 18,
"authorityMode": "external",
"authority": "2VavnvNLSTNUWhaG4Tkdw86WrRJx5dyoyQYij6KQzz3Y",
"metadataStandard": "Expanded",
"metadataUri": "data:application/json;base64,eyJuYW1lIjoiUG9ja2V0IExpbnQgRGVsdXhlIiwiZGVzY3JpcHRpb24iOiJQcmVtaXVtIGxpbnQsIG5vdyB0b2tlbml6ZWQuIiwibW9kZSI6ImV4dGVybmFsLWF1dGhvcml0eSIsImF1dGhvcml0eSI6IjJWYXZudk5MU1ROVVdoYUc0VGtkdzg2V3JSSng1ZHlveVFZaWo2S1F6ejNZIn0=",
"creators": "2VavnvNLSTNUWhaG4Tkdw86WrRJx5dyoyQYij6KQzz3Y",
"description": "Premium lint, now tokenized.",
"source": "testnet",
"instruction": "new_definition_with_metadata",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "Hqfz9dfGeMgWbL9rhUnMPLgdQZUMczjNjUWCTRozkkk7",
"hex": "fa32f354408857006f8ea396b0419823bd04436eadb2d273d2618a46b4793ed8",
"name": "Pocket Lint Deluxe",
"type": "fungible",
"totalSupplyRaw": "10000000000000000000000000",
"mintAuthority": "2VavnvNLSTNUWhaG4Tkdw86WrRJx5dyoyQYij6KQzz3Y",
"metadataId": "GciAureLmwkCKuELgo2YzF1dYhh8Jcu7cMciquuY7no5"
},
"holding": {
"id": "7aDup3sUWkFF2RZnCXyo78BEvaKT3Wf4dWrPTqaMeRXj",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "10000000000000000000000000",
"displayBalance": "10,000,000"
},
"holdings": [
{
"id": "7aDup3sUWkFF2RZnCXyo78BEvaKT3Wf4dWrPTqaMeRXj",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "10000000000000000000000000",
"displayBalance": "10,000,000"
}
],
"metadata": {
"id": "GciAureLmwkCKuELgo2YzF1dYhh8Jcu7cMciquuY7no5",
"standard": "Expanded",
"uri": "data:application/json;base64,eyJuYW1lIjoiUG9ja2V0IExpbnQgRGVsdXhlIiwiZGVzY3JpcHRpb24iOiJQcmVtaXVtIGxpbnQsIG5vdyB0b2tlbml6ZWQuIiwibW9kZSI6ImV4dGVybmFsLWF1dGhvcml0eSIsImF1dGhvcml0eSI6IjJWYXZudk5MU1ROVVdoYUc0VGtkdzg2V3JSSng1ZHlveVFZaWo2S1F6ejNZIn0=",
"creators": "2VavnvNLSTNUWhaG4Tkdw86WrRJx5dyoyQYij6KQzz3Y",
"description": "Premium lint, now tokenized."
}
},
{
"id": "14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La",
"name": "Sir Mints-a-Lot",
"symbol": "",
"type": "fungible",
"definitionId": "14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La",
"definitionHex": "00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb",
"holdingId": "B1yzSPqaetRJx19aXUd7xpQje5iYNF2Qwr1SKbFLCf8F",
"metadataId": "21ByKA4ZCYWm8pfPyhR1Q1tqYT2RRA8JGsDBX1cp24c7",
"rawSupply": "1000000000000000000",
"displaySupply": "1,000,000,000",
"inferredDecimals": 9,
"authorityMode": "self",
"authority": "14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La",
"metadataStandard": "Simple",
"metadataUri": "data:application/json;base64,eyJuYW1lIjoiU2lyIE1pbnRzLWEtTG90IiwiZGVzY3JpcHRpb24iOiJBIHNlbGYtYXV0aG9yaXplZCBtaW50aW5nIGVudGh1c2lhc3QuIiwibW9kZSI6InNlbGYtYXV0aG9yaXR5IiwiYXV0aG9yaXR5IjoiMTR0QXRpeE1CeUZ5SnJjWlZ5V2liaXRuaWpMZ2Q1OVBmeXJqZG5Zem84TGEifQ==",
"creators": "14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La",
"description": "A self-authorized minting enthusiast.",
"source": "testnet",
"instruction": "new_definition_with_metadata",
"printableCopies": null,
"masterHolding": null,
"definition": {
"id": "14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La",
"hex": "00fe99e4fbd4c71f92e47c384c6235244c8cce39b6d6367e1e338eca0ffe01cb",
"name": "Sir Mints-a-Lot",
"type": "fungible",
"totalSupplyRaw": "1000000000000000000",
"mintAuthority": "14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La",
"metadataId": "21ByKA4ZCYWm8pfPyhR1Q1tqYT2RRA8JGsDBX1cp24c7"
},
"holding": {
"id": "B1yzSPqaetRJx19aXUd7xpQje5iYNF2Qwr1SKbFLCf8F",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "1000000000000000000",
"displayBalance": "1,000,000,000"
},
"holdings": [
{
"id": "B1yzSPqaetRJx19aXUd7xpQje5iYNF2Qwr1SKbFLCf8F",
"wallet": "all-assets",
"role": "treasury",
"rawBalance": "1000000000000000000",
"displayBalance": "1,000,000,000"
}
],
"metadata": {
"id": "21ByKA4ZCYWm8pfPyhR1Q1tqYT2RRA8JGsDBX1cp24c7",
"standard": "Simple",
"uri": "data:application/json;base64,eyJuYW1lIjoiU2lyIE1pbnRzLWEtTG90IiwiZGVzY3JpcHRpb24iOiJBIHNlbGYtYXV0aG9yaXplZCBtaW50aW5nIGVudGh1c2lhc3QuIiwibW9kZSI6InNlbGYtYXV0aG9yaXR5IiwiYXV0aG9yaXR5IjoiMTR0QXRpeE1CeUZ5SnJjWlZ5V2liaXRuaWpMZ2Q1OVBmeXJqZG5Zem84TGEifQ==",
"creators": "14tAtixMByFyJrcZVyWibitnijLgd59PfyrjdnYzo8La",
"description": "A self-authorized minting enthusiast."
}
},
{
"id": "3sG2zN3fAXBvs9mdgFHFiJFSFiwiYyaR5HUA3gZZTSXt",
"name": "Glitchlings",
"symbol": "",
"type": "nonFungible",
"definitionId": "3sG2zN3fAXBvs9mdgFHFiJFSFiwiYyaR5HUA3gZZTSXt",
"definitionHex": "2a9769e40f12e6d1567bea757974fd9cbb504be1bd8e5a262e6ee5bfaf533d53",
"holdingId": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv",
"metadataId": "BxAbq1EK6Hg6y8g296719V9HQJSet4yio5VG9Ub3YHtU",
"rawSupply": null,
"displaySupply": null,
"inferredDecimals": null,
"authorityMode": "masterHolding",
"authority": null,
"metadataStandard": "Expanded",
"metadataUri": "data:application/json;base64,eyJuYW1lIjoiR2xpdGNobGluZ3MiLCJkZXNjcmlwdGlvbiI6IkxFWiB0ZXN0bmV0IG5vbi1mdW5naWJsZSBjb2xsZWN0aW9uIiwibmV0d29yayI6InRlc3RuZXQiLCJhdXRob3JpdHkiOiI1UVRCRlFwWURaRHJqNTY5V1BCYW1WcWRhdU5rUlJvNERyWUU0WlpabjJQdiJ9",
"creators": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv",
"description": "LEZ testnet non-fungible collection",
"source": "testnet",
"instruction": "new_definition_with_metadata",
"printableCopies": 64,
"masterHolding": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv",
"definition": {
"id": "3sG2zN3fAXBvs9mdgFHFiJFSFiwiYyaR5HUA3gZZTSXt",
"hex": "2a9769e40f12e6d1567bea757974fd9cbb504be1bd8e5a262e6ee5bfaf533d53",
"name": "Glitchlings",
"type": "nonFungible",
"printableSupply": 64,
"metadataId": "BxAbq1EK6Hg6y8g296719V9HQJSet4yio5VG9Ub3YHtU"
},
"holding": {
"id": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv",
"wallet": "all-assets",
"role": "nftMaster",
"printBalance": 63,
"printAuthority": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv"
},
"holdings": [
{
"id": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv",
"wallet": "all-assets",
"role": "nftMaster",
"printBalance": 63,
"printAuthority": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv"
},
{
"id": "96b6C9RCd5WCa42RgaRMT7ePEJh5GXLLhbBxd15qwA1i",
"role": "nftPrintedCopy",
"owned": true
}
],
"printedCopies": [
{
"id": "96b6C9RCd5WCa42RgaRMT7ePEJh5GXLLhbBxd15qwA1i",
"owned": true
}
],
"metadata": {
"id": "BxAbq1EK6Hg6y8g296719V9HQJSet4yio5VG9Ub3YHtU",
"standard": "Expanded",
"uri": "data:application/json;base64,eyJuYW1lIjoiR2xpdGNobGluZ3MiLCJkZXNjcmlwdGlvbiI6IkxFWiB0ZXN0bmV0IG5vbi1mdW5naWJsZSBjb2xsZWN0aW9uIiwibmV0d29yayI6InRlc3RuZXQiLCJhdXRob3JpdHkiOiI1UVRCRlFwWURaRHJqNTY5V1BCYW1WcWRhdU5rUlJvNERyWUU0WlpabjJQdiJ9",
"creators": "5QTBFQpYDZDrj569WPBamVqdauNkRRo4DrYE4ZZZn2Pv",
"description": "LEZ testnet non-fungible collection"
}
}
]
property var draftDefinitions: []
readonly property var allDefinitions: root.fixtureDefinitions.concat(root.draftDefinitions)
function findDefinition(id) {
var wantedId = String(id || "")
for (var index = 0; index < root.allDefinitions.length; ++index) {
var definition = root.allDefinitions[index]
if (definition.id === wantedId || definition.definitionId === wantedId)
return definition
}
return null
}
function visibleDefinitions(query) {
var search = String(query || "").trim().toLowerCase()
if (!search)
return root.allDefinitions
var visible = []
for (var index = 0; index < root.allDefinitions.length; ++index) {
var definition = root.allDefinitions[index]
var fields = [definition.name, definition.id, definition.definitionId, definition.type]
for (var fieldIndex = 0; fieldIndex < fields.length; ++fieldIndex) {
if (String(fields[fieldIndex] || "").toLowerCase().indexOf(search) !== -1) {
visible.push(definition)
break
}
}
}
return visible
}
function shortAddress(address) {
var value = String(address || "")
return value.length > 13
? value.substring(0, 6) + "..." + value.substring(value.length - 4)
: value
}
function addDraft(draft) {
if (!draft)
return null
root.draftDefinitions = root.draftDefinitions.concat([draft])
return draft
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

+6
View File
@@ -0,0 +1,6 @@
#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.
+26
View File
@@ -0,0 +1,26 @@
#ifndef TOKEN_UI_BACKEND_H
#define TOKEN_UI_BACKEND_H
#include <QObject>
#include "rep_TokenUiBackend_source.h"
#include "AccountModel.h"
#include "WalletBackendLogic.h"
class LogosAPI;
// 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> {
Q_OBJECT
Q_PROPERTY(AccountModel* accountModel READ accountModel CONSTANT)
public:
explicit TokenUiBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr)
: WalletBackendLogic(logosAPI, parent, "token_ui", "TokenUI") {}
};
#endif // TOKEN_UI_BACKEND_H
+46
View File
@@ -0,0 +1,46 @@
// QtRO view contract for the Token UI backend. PROPs auto-sync to every QML
// replica; SLOTs are the async surface QML calls via logos.watch(...).
// The account list is exposed separately as a Q_PROPERTY model on the backend
// (reached from QML via logos.model("token_ui", "accountModel")).
class TokenUiBackend
{
PROP(bool isWalletOpen READONLY)
PROP(bool walletExists READONLY)
PROP(QString configPath READONLY)
PROP(QString storagePath READONLY)
PROP(QString walletHome READONLY)
PROP(int lastSyncedBlock READONLY)
PROP(int currentBlockHeight READONLY)
PROP(QString sequencerAddr READONLY)
// Whether the configured sequencer answered the last reachability probe.
// Defaults true so the UI doesn't flash a warning before the first check.
PROP(bool sequencerReachable READONLY)
// Account management
SLOT(QString createAccountPublic())
SLOT(QString createAccountPrivate())
SLOT(void refreshAccounts())
SLOT(void refreshBalances())
SLOT(QString getBalance(QString accountIdHex, bool isPublic))
// Wallet lifecycle. createNewDefault() is the happy path: it creates a
// fresh per-app wallet at walletHome with no path picking. createNew()
// keeps explicit paths for an "advanced" flow. Both return the new wallet's
// BIP39 mnemonic (empty on failure) so the UI can force a seed-phrase backup
// before the wallet is usable — this is the only chance to record it.
SLOT(QString createNewDefault(QString password))
SLOT(QString createNew(QString configPath, QString storagePath, QString password))
// Re-open the existing on-disk wallet after a disconnect.
SLOT(bool openExisting())
// Close this app's wallet view (lock); does not delete the wallet and, in
// 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))
// Misc
SLOT(void copyToClipboard(QString text))
}
+19
View File
@@ -0,0 +1,19 @@
#include "TokenUiPlugin.h"
#include "TokenUiBackend.h"
#include <QDebug>
TokenUiPlugin::TokenUiPlugin(QObject* parent)
: QObject(parent)
{
}
TokenUiPlugin::~TokenUiPlugin() = default;
void TokenUiPlugin::initLogos(LogosAPI* api)
{
if (m_backend) return;
m_backend = new TokenUiBackend(api, this);
setBackend(m_backend);
qDebug() << "TokenUiPlugin: backend initialized";
}
+38
View File
@@ -0,0 +1,38 @@
#ifndef TOKEN_UI_PLUGIN_H
#define TOKEN_UI_PLUGIN_H
#include <QObject>
#include <QString>
#include <QtPlugin> // for Q_PLUGIN_METADATA, Q_INTERFACES
#include "TokenUiPluginInterface.h"
#include "LogosViewPluginBase.h"
class LogosAPI;
class TokenUiBackend;
// Thin plugin entry point. Holds a TokenUiBackend and lets the generated
// view-plugin base expose it to ui-host.
class TokenUiPlugin : public QObject,
public TokenUiPluginInterface,
public TokenUiBackendViewPluginBase
{
Q_OBJECT
Q_PLUGIN_METADATA(IID TokenUiPluginInterface_iid FILE "../metadata.json")
Q_INTERFACES(TokenUiPluginInterface)
public:
explicit TokenUiPlugin(QObject* parent = nullptr);
~TokenUiPlugin() override;
QString name() const override { return "token_ui"; }
QString version() const override { return "0.1.0"; }
// Called by ui-host after plugin load. Creates the backend and wires it
// up with the provided LogosAPI.
Q_INVOKABLE void initLogos(LogosAPI* api);
private:
TokenUiBackend* m_backend = nullptr;
};
#endif // TOKEN_UI_PLUGIN_H
+19
View File
@@ -0,0 +1,19 @@
#ifndef TOKEN_UI_PLUGIN_INTERFACE_H
#define TOKEN_UI_PLUGIN_INTERFACE_H
#include <QtPlugin> // for Q_DECLARE_INTERFACE
#include "interface.h"
// Marker interface used by Qt's plugin loader to identify the Token UI plugin.
// The actual API surface (slots, properties, signals) lives in
// TokenUiBackend.rep — this header only carries the IID.
class TokenUiPluginInterface : public PluginInterface
{
public:
virtual ~TokenUiPluginInterface() = default;
};
#define TokenUiPluginInterface_iid "org.logos.TokenUiPluginInterface"
Q_DECLARE_INTERFACE(TokenUiPluginInterface, TokenUiPluginInterface_iid)
#endif // TOKEN_UI_PLUGIN_INTERFACE_H