pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls.Basic import QtQuick.Layouts Item { id: root property var wallet: null property var portfolio: null property var network: null property var accountModel: null property var watchCall: null property bool compact: false property real viewportWidth: width property int selectedIndex: -1 property bool busy: false property bool openPending: false property bool advancedExpanded: false property bool availableExpanded: false property bool primarySelectionQueued: false property string postCreationWarning: "" property bool createdWalletAwaitingAcknowledgement: false property string reportedOpenErrorKey: "" readonly property bool connected: root.wallet !== null && root.wallet.isWalletOpen readonly property string syncStatus: root.wallet ? String(root.wallet.walletSyncStatus || "closed") : "closed" readonly property bool compactLayout: root.compact || root.viewportWidth < 680 readonly property bool walletOpening: root.wallet !== null && (root.wallet.walletSyncStatus === "opening" || root.wallet.walletSyncStatus === "syncing") readonly property string selectedAddress: root.accountAt(root.selectedIndex, "address") readonly property string selectedDisplayAddress: root.accountAt(root.selectedIndex, "displayAddress") readonly property string selectedName: root.accountAt(root.selectedIndex, "name") readonly property bool selectedIsPublic: root.accountAt(root.selectedIndex, "isPublic") === true readonly property var portfolioProvider: root.portfolio ? root.portfolio : root.wallet readonly property var networkProvider: root.network ? root.network : root.wallet readonly property string activeNetwork: root.networkProvider && root.networkProvider.activeNetwork ? root.networkProvider.activeNetwork : "" readonly property string networkStatus: root.networkProvider && root.networkProvider.networkStatus !== undefined ? root.networkProvider.networkStatus : "" readonly property string assetStatus: root.portfolioProvider && root.portfolioProvider.assetStatus !== undefined ? root.portfolioProvider.assetStatus : "" readonly property string assetError: root.portfolioProvider && root.portfolioProvider.assetError ? root.portfolioProvider.assetError : "" readonly property var walletAssets: root.portfolioProvider && root.portfolioProvider.assets ? root.portfolioProvider.assets : [] readonly property int availableAssetCount: root.assetCount("available") readonly property string primaryName: root.wallet && root.wallet.primaryAccountName ? root.wallet.primaryAccountName : root.selectedName implicitWidth: root.connected ? connectedButton.implicitWidth : connectButton.implicitWidth implicitHeight: 40 Instantiator { id: accounts model: root.accountModel delegate: QtObject { required property string address required property string displayAddress required property string name required property bool isPublic required property bool isPrimary required property bool canBePrimary required property string kind } onCountChanged: { root.syncPrimarySelection() root.schedulePrimarySelection() } onObjectAdded: root.schedulePrimarySelection() } function accountAt(index, field) { const entry = index >= 0 && index < accounts.count ? accounts.objectAt(index) : null return entry ? entry[field] : (field === "isPublic" ? false : "") } function assetCount(sectionName) { let count = 0 for (const asset of root.walletAssets) { if (asset.section === sectionName) ++count } return count } function syncPrimarySelection() { if (accounts.count === 0) { root.selectedIndex = -1 return } const requested = root.wallet && root.wallet.primaryAccountAddress ? root.wallet.primaryAccountAddress : "" for (let index = 0; index < accounts.count; ++index) { const account = accounts.objectAt(index) if (!account) continue if ((requested.length > 0 && account.address === requested) || account.isPrimary) { root.selectedIndex = index return } } for (let index = 0; index < accounts.count; ++index) { const account = accounts.objectAt(index) if (!account) continue if (account.kind === "user" && account.canBePrimary) { root.selectedIndex = index return } } root.selectedIndex = -1 } function schedulePrimarySelection() { if (root.primarySelectionQueued) return root.primarySelectionQueued = true Qt.callLater(function() { root.primarySelectionQueued = false if (root.connected) root.syncPrimarySelection() else root.selectedIndex = -1 }) } function shortAddress(address) { return address && address.length > 13 ? address.substring(0, 6) + "…" + address.substring(address.length - 4) : address || "" } function watchResult(result, success, failure) { if (root.watchCall) root.watchCall(result, success, failure) else success(result) } function showError(message) { messageDialog.message = message messageDialog.open() } function walletRefreshWarning(subject) { if (!root.wallet || root.wallet.walletSyncStatus !== "error") return "" return qsTr("%1 was created, but could not be refreshed. Reconnect the wallet to refresh it.") .arg(subject) } function openFailureKey() { if (!root.wallet || root.wallet.walletSyncStatus !== "error") return "" return root.wallet.walletSyncError || "unknown" } function openFailureMessage() { const error = root.wallet ? root.wallet.walletSyncError : "" return error ? qsTr("Wallet could not be opened: %1").arg(error) : qsTr("Wallet could not be opened.") } function reportUnhandledOpenFailure() { if (!root.wallet || root.openPending || root.wallet.isWalletOpen || root.wallet.walletSyncStatus !== "error") { return } const key = root.openFailureKey() if (key === root.reportedOpenErrorKey) return root.reportedOpenErrorKey = key root.busy = false root.showError(root.openFailureMessage()) } function openAccepted(result) { return result === true || result === "true" || result === 1 || result === "1" } function finishOpen() { root.openPending = false root.busy = false } function failOpen(message) { if (!root.openPending) return root.reportedOpenErrorKey = root.openFailureKey() root.finishOpen() root.showError(message) } function settleOpenFromWalletState() { if (!root.openPending || !root.wallet) return const status = root.wallet.walletSyncStatus if (status === undefined) { root.finishOpen() return } if (status === "ready" && root.wallet.isWalletOpen) { root.finishOpen() return } if (status === "error") { root.failOpen(root.openFailureMessage()) return } if (status === "closed" && root.wallet.walletExists === false) root.failOpen(qsTr("Wallet could not be opened.")) } function openWallet() { if (!root.wallet || root.busy || root.walletOpening) return root.busy = true root.openPending = true try { root.watchResult(root.wallet.openExisting(), function(ok) { if (!root.openAccepted(ok)) { root.failOpen(qsTr("Wallet could not be opened.")) return } Qt.callLater(root.settleOpenFromWalletState) }, function(error) { root.failOpen(qsTr("Wallet could not be opened: %1").arg(error)) }) } catch (error) { root.failOpen(qsTr("Wallet could not be opened: %1").arg(error)) } } function makePrimary(address) { if (!root.wallet || !address) return try { root.watchResult(root.wallet.setPrimaryAccount(address), function(ok) { if (!ok) root.showError(qsTr("This account cannot be primary.")) root.syncPrimarySelection() }, function(error) { root.showError(qsTr("Primary account could not be changed: %1").arg(error)) }) } catch (error) { root.showError(qsTr("Primary account could not be changed: %1").arg(error)) } } TextEdit { id: clipboardProxy visible: false } function copyToClipboard(text) { if (!text) return clipboardProxy.text = text clipboardProxy.selectAll() clipboardProxy.copy() clipboardProxy.deselect() clipboardProxy.text = "" } Connections { target: root.accountModel ignoreUnknownSignals: true function onModelReset() { root.syncPrimarySelection() root.schedulePrimarySelection() } function onRowsInserted() { root.syncPrimarySelection() root.schedulePrimarySelection() } function onRowsRemoved() { root.syncPrimarySelection() root.schedulePrimarySelection() } function onDataChanged() { root.syncPrimarySelection() if (root.selectedIndex < 0) root.schedulePrimarySelection() } } Connections { target: root.wallet ignoreUnknownSignals: true function onPrimaryAccountAddressChanged() { root.syncPrimarySelection() root.schedulePrimarySelection() } function onWalletSyncStatusChanged() { if (!root.wallet || root.wallet.walletSyncStatus !== "error") root.reportedOpenErrorKey = "" Qt.callLater(root.settleOpenFromWalletState) Qt.callLater(root.reportUnhandledOpenFailure) } function onWalletSyncErrorChanged() { Qt.callLater(root.settleOpenFromWalletState) Qt.callLater(root.reportUnhandledOpenFailure) } function onIsWalletOpenChanged() { Qt.callLater(root.settleOpenFromWalletState) } function onWalletExistsChanged() { Qt.callLater(root.settleOpenFromWalletState) } } Component.onCompleted: Qt.callLater(root.reportUnhandledOpenFailure) onConnectedChanged: { if (!root.connected) { root.selectedIndex = -1 walletMenu.close() } else { root.syncPrimarySelection() root.schedulePrimarySelection() } } onViewportWidthChanged: { if (walletMenu.opened) Qt.callLater(walletMenu.updateAnchor) } Button { id: connectButton objectName: "walletConnectButton" anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter visible: !root.connected enabled: root.wallet !== null && !root.busy && !root.walletOpening implicitHeight: 40 implicitWidth: root.compactLayout ? 40 : 108 text: root.compactLayout ? "" : (root.busy || root.walletOpening ? qsTr("Connecting…") : qsTr("Connect")) icon.source: Qt.resolvedUrl("icons/account.svg") Accessible.name: qsTr("Connect AMM Wallet") ToolTip.text: Accessible.name ToolTip.visible: hovered && root.compactLayout background: Rectangle { color: connectButton.pressed ? "#d97706" : "#f59e0b" radius: 8 } contentItem: RowLayout { spacing: 6 Image { visible: root.compactLayout source: connectButton.icon.source sourceSize.width: 18 sourceSize.height: 18 } Label { Layout.fillWidth: true visible: !root.compactLayout text: connectButton.text color: "#18181b" font.bold: true horizontalAlignment: Text.AlignHCenter } } onClicked: { if (root.wallet && root.wallet.walletExists) root.openWallet() else createWalletDialog.open() } } Button { id: connectedButton objectName: "walletAccountButton" anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter visible: root.connected enabled: !root.busy implicitHeight: 40 implicitWidth: root.compactLayout ? 44 : Math.max(176, accountButtonLabel.implicitWidth + 54) Accessible.name: qsTr("AMM Wallet, primary account %1").arg(root.primaryName) background: Rectangle { color: connectedButton.pressed ? "#3f3f46" : "#27272a" border.width: walletMenu.opened || connectedButton.activeFocus ? 1 : 0 border.color: "#f59e0b" radius: 8 } contentItem: RowLayout { spacing: 8 Rectangle { Layout.preferredWidth: 8 Layout.preferredHeight: 8 radius: 4 objectName: "walletNetworkStatusIndicator" color: !root.networkProvider || root.networkStatus.length === 0 || root.networkStatus === "ready" ? "#22c55e" : root.networkStatus === "loading" ? "#f59e0b" : "#ef4444" } Label { id: accountButtonLabel Layout.fillWidth: true visible: !root.compactLayout text: root.primaryName.length > 0 ? qsTr("AMM Wallet · %1").arg(root.primaryName) : qsTr("AMM Wallet") color: "#fafafa" font.bold: true elide: Text.ElideRight } Label { visible: !root.compactLayout text: walletMenu.opened ? "▴" : "▾" color: "#a1a1aa" } } onClicked: { if (walletMenu.opened || Date.now() - walletMenu.lastClosedMs < 200) walletMenu.close() else walletMenu.open() } } Popup { id: walletMenu objectName: "walletMenu" palette.windowText: "#d4d4d8" property real lastClosedMs: 0 property point anchorPosition: Qt.point(0, 0) readonly property var viewport: Overlay.overlay readonly property real spaceAbove: Math.max(0, anchorPosition.y - 20) readonly property real spaceBelow: viewport ? Math.max(0, viewport.height - anchorPosition.y - connectedButton.height - 20) : implicitHeight readonly property bool opensAbove: spaceBelow < implicitHeight && spaceAbove > spaceBelow readonly property real availableMenuHeight: opensAbove ? spaceAbove : spaceBelow parent: connectedButton x: viewport ? Math.max(12 - anchorPosition.x, Math.min(connectedButton.width - width, viewport.width - width - 12 - anchorPosition.x)) : connectedButton.width - width y: opensAbove ? -height - 8 : connectedButton.height + 8 width: Math.min(400, Math.max(0, Math.min(root.viewportWidth, viewport ? viewport.width : root.viewportWidth) - 24)) height: Math.min(implicitHeight, availableMenuHeight) margins: 12 padding: 12 focus: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside function updateAnchor() { if (viewport) anchorPosition = connectedButton.mapToItem(viewport, 0, 0) } onAboutToShow: updateAnchor() onClosed: { walletMenu.lastClosedMs = Date.now() if (walletStack.depth > 1) walletStack.pop(null, StackView.Immediate) } Connections { target: walletMenu.opened ? walletMenu.viewport : null function onWidthChanged() { Qt.callLater(walletMenu.updateAnchor) } function onHeightChanged() { Qt.callLater(walletMenu.updateAnchor) } } background: Rectangle { color: "#18181b" border.color: "#3f3f46" border.width: 1 radius: 10 } contentItem: StackView { id: walletStack objectName: "walletStack" clip: true width: walletMenu.availableWidth height: walletMenu.availableHeight implicitWidth: walletMenu.availableWidth implicitHeight: currentItem ? currentItem.implicitHeight : 0 initialItem: walletOverview } Component { id: walletOverview ScrollView { implicitHeight: Math.min(overviewContent.implicitHeight, 520) contentWidth: availableWidth ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ColumnLayout { id: overviewContent objectName: "walletOverviewContent" width: parent.width spacing: 12 RowLayout { Layout.fillWidth: true ColumnLayout { Layout.fillWidth: true spacing: 1 Label { text: qsTr("AMM Wallet") color: "#fafafa" font.bold: true font.pixelSize: 16 } Label { objectName: "walletNetworkName" text: root.activeNetwork || qsTr("Network unavailable") color: "#a1a1aa" font.pixelSize: 11 } } WalletIconButton { objectName: "walletAccountsButton" iconSource: Qt.resolvedUrl("icons/account.svg") accessibleName: qsTr("Accounts") onClicked: walletStack.push(accountList, StackView.Immediate) } WalletIconButton { objectName: "walletDisconnectButton" iconSource: Qt.resolvedUrl("icons/power.svg") accessibleName: qsTr("Disconnect") onClicked: { walletMenu.close() if (root.wallet) root.wallet.disconnectWallet() } } } Rectangle { Layout.fillWidth: true implicitHeight: identityCard.implicitHeight + 24 color: "#27272a" radius: 8 ColumnLayout { id: identityCard anchors.fill: parent anchors.margins: 12 spacing: 7 RowLayout { Layout.fillWidth: true Label { Layout.fillWidth: true text: root.primaryName || qsTr("No primary account") color: "#fafafa" font.bold: true elide: Text.ElideRight } Label { text: qsTr("Primary") color: "#fbbf24" font.pixelSize: 11 font.bold: true } } Label { objectName: "walletPrimaryAccountType" visible: root.selectedIndex >= 0 text: root.selectedIsPublic ? qsTr("Public user account") : qsTr("Private account") color: "#a1a1aa" font.pixelSize: 11 } RowLayout { Layout.fillWidth: true spacing: 4 Label { Layout.fillWidth: true text: root.selectedDisplayAddress color: "#71717a" font.family: "monospace" font.pixelSize: 11 elide: Text.ElideMiddle } CopyButton { visible: root.selectedDisplayAddress.length > 0 copyText: root.selectedDisplayAddress copyLabel: qsTr("Copy address") } } } } Label { text: qsTr("Assets") color: "#fafafa" font.bold: true } Label { objectName: "walletAssetsLoadingLabel" visible: root.portfolioProvider && root.assetStatus === "loading" text: qsTr("Loading balances…") color: "#a1a1aa" } Repeater { objectName: "walletAssetRepeater" model: root.walletAssets delegate: Rectangle { required property var modelData objectName: "walletAssetBox" Layout.fillWidth: true visible: modelData.section === "assets" implicitHeight: visible ? 68 : 0 color: "#27272a" radius: 10 border.width: 1 border.color: "#3f3f46" Accessible.name: qsTr("%1 token, balance %2") .arg(modelData.name).arg(modelData.balance) RowLayout { anchors.fill: parent anchors.margins: 12 ColumnLayout { Layout.fillWidth: true spacing: 1 Label { Layout.fillWidth: true text: modelData.name color: "#fafafa" font.bold: true elide: Text.ElideRight } Label { Layout.fillWidth: true text: root.shortAddress(modelData.displayDefinitionId || modelData.definitionId) color: "#71717a" font.family: "monospace" font.pixelSize: 10 } } Label { text: modelData.balance color: "#fafafa" font.family: "monospace" font.bold: true } CopyButton { copyText: modelData.displayDefinitionId || modelData.definitionId copyLabel: qsTr("Copy token ID") } } } } Label { visible: (!root.walletAssets || root.walletAssets.length === 0) && (!root.portfolioProvider || root.assetStatus !== "loading") text: root.assetError ? qsTr("Assets unavailable: %1").arg(root.assetError) : qsTr("No assets yet") color: "#a1a1aa" wrapMode: Text.Wrap } Button { objectName: "walletAvailableAssetsButton" Layout.fillWidth: true visible: root.availableAssetCount > 0 text: root.availableExpanded ? qsTr("Hide available tokens") : qsTr("Available tokens") flat: true onClicked: root.availableExpanded = !root.availableExpanded } Repeater { objectName: "walletAvailableAssetRepeater" model: root.walletAssets delegate: Rectangle { required property var modelData objectName: "walletAvailableAssetBox" Layout.fillWidth: true visible: root.availableExpanded && modelData.section === "available" implicitHeight: visible ? 64 : 0 color: "#202023" radius: 10 border.width: 1 border.color: "#3f3f46" Accessible.name: qsTr("%1 token, no balance") .arg(modelData.name) RowLayout { anchors.fill: parent anchors.margins: 12 ColumnLayout { Layout.fillWidth: true spacing: 1 Label { Layout.fillWidth: true text: modelData.name color: modelData.status === "ready" ? "#d4d4d8" : "#a1a1aa" elide: Text.ElideRight } Label { text: root.shortAddress(modelData.displayDefinitionId || modelData.definitionId) color: "#71717a" font.family: "monospace" font.pixelSize: 10 } } Label { text: modelData.status === "ready" ? "0" : qsTr("Unavailable") color: "#71717a" font.pixelSize: 11 } CopyButton { copyText: modelData.displayDefinitionId || modelData.definitionId copyLabel: qsTr("Copy token ID") } } } } } } } Component { id: accountList ColumnLayout { spacing: 10 RowLayout { Layout.fillWidth: true WalletIconButton { objectName: "walletAccountsBackButton" iconSource: Qt.resolvedUrl("icons/back.svg") accessibleName: qsTr("Back") onClicked: walletStack.pop(null, StackView.Immediate) } Label { Layout.fillWidth: true text: qsTr("Accounts") color: "#fafafa" font.bold: true } } Label { Layout.fillWidth: true visible: walletMenu.availableMenuHeight >= 280 text: qsTr("Choose the account used as your wallet identity. Program records stay under Advanced.") color: "#a1a1aa" font.pixelSize: 11 wrapMode: Text.Wrap } ListView { id: accountListView objectName: "walletAccountList" Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 48 Layout.preferredHeight: Math.min(contentHeight, 300) clip: true spacing: 0 model: root.accountModel ScrollIndicator.vertical: ScrollIndicator { } delegate: Item { id: accountWrapper required property string name required property string alias required property string address required property string displayAddress required property string kind required property string section required property string programName required property string accountType required property string decodedData required property string visibility required property bool canBePrimary required property bool isPrimary readonly property bool shown: section === "accounts" || (root.advancedExpanded && section === "advanced") width: ListView.view.width height: shown ? accountDelegate.implicitHeight + 6 : 0 visible: shown AccountDelegate { id: accountDelegate width: parent.width name: accountWrapper.name alias: accountWrapper.alias address: accountWrapper.address displayAddress: accountWrapper.displayAddress kind: accountWrapper.kind section: accountWrapper.section programName: accountWrapper.programName accountType: accountWrapper.accountType decodedData: accountWrapper.decodedData visibility: accountWrapper.visibility canBePrimary: accountWrapper.canBePrimary isPrimary: accountWrapper.isPrimary onMakePrimaryRequested: function(address) { root.makePrimary(address) } onRenameRequested: function(address, alias) { renameDialog.accountAddress = address renameField.text = alias renameDialog.open() } } } } RowLayout { Layout.fillWidth: true spacing: 6 Button { objectName: "walletAdvancedAccountsButton" Layout.fillWidth: true text: root.advancedExpanded ? qsTr("Hide Advanced") : qsTr("Advanced") flat: true onClicked: root.advancedExpanded = !root.advancedExpanded } Button { objectName: "walletAddAccountButton" Layout.fillWidth: true text: qsTr("Add account") enabled: !root.busy onClicked: createAccountDialog.open() } } } } } Dialog { id: renameDialog objectName: "walletRenameDialog" property string accountAddress: "" parent: Overlay.overlay modal: true anchors.centerIn: parent width: Math.min(360, parent ? parent.width - 32 : 360) title: qsTr("Rename account") standardButtons: Dialog.Save | Dialog.Cancel TextField { id: renameField objectName: "walletAliasField" width: parent.width maximumLength: 40 placeholderText: qsTr("Account name") Accessible.name: qsTr("Account name") } onAccepted: { if (!root.wallet) return try { root.watchResult(root.wallet.setAccountAlias(accountAddress, renameField.text), function(ok) { if (!ok) root.showError(qsTr("Account name could not be saved.")) }, function(error) { root.showError(qsTr("Account name could not be saved: %1").arg(error)) }) } catch (error) { root.showError(qsTr("Account name could not be saved: %1").arg(error)) } } } CreateWalletDialog { id: createWalletDialog objectName: "createWalletDialog" walletHome: root.wallet ? root.wallet.walletHome || "" : "" busy: root.busy onCreateRequested: function(password) { if (!root.wallet || root.busy) return root.postCreationWarning = "" root.createdWalletAwaitingAcknowledgement = false root.busy = true try { root.watchResult(root.wallet.createNewDefault(password), function(mnemonic) { root.busy = false if (mnemonic && mnemonic.length > 0) { createWalletDialog.mnemonic = mnemonic root.createdWalletAwaitingAcknowledgement = true root.postCreationWarning = root.walletRefreshWarning(qsTr("Wallet")) } else createWalletDialog.errorText = qsTr("Wallet could not be created.") }, function(error) { root.busy = false createWalletDialog.errorText = qsTr("Wallet could not be created: %1").arg(error) }) } catch (error) { root.busy = false createWalletDialog.errorText = qsTr("Wallet could not be created: %1").arg(error) } } onCopyRequested: function(text) { root.copyToClipboard(text) } onClosed: { const warning = root.postCreationWarning.length > 0 ? root.postCreationWarning : root.createdWalletAwaitingAcknowledgement ? root.walletRefreshWarning(qsTr("Wallet")) : "" root.postCreationWarning = "" root.createdWalletAwaitingAcknowledgement = false if (warning.length > 0) root.showError(warning) } } CreateAccountDialog { id: createAccountDialog objectName: "createAccountDialog" busy: root.busy onCreateRequested: function(isPublic) { if (!root.wallet || root.busy) return root.busy = true try { const request = isPublic ? root.wallet.createAccountPublic() : root.wallet.createAccountPrivate() root.watchResult(request, function(accountId) { root.busy = false if (accountId && accountId.length > 0) { createAccountDialog.close() Qt.callLater(function() { const warning = root.walletRefreshWarning(qsTr("Account")) if (warning.length > 0) root.showError(warning) }) } else root.showError(qsTr("Account could not be created.")) }, function(error) { root.busy = false root.showError(qsTr("Account could not be created: %1").arg(error)) }) } catch (error) { root.busy = false root.showError(qsTr("Account could not be created: %1").arg(error)) } } } WalletMessageDialog { id: messageDialog objectName: "walletMessageDialog" } }