feat(token): add token definition app
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
```
|
||||
@@ -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});
|
||||
};
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 150 KiB |
@@ -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.
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||