Native NFT Collections UI: gallery, detail/verify, mint-from-file, gift - #26
Draft
RhettCreighton wants to merge 14 commits into
Draft
Native NFT Collections UI: gallery, detail/verify, mint-from-file, gift#26RhettCreighton wants to merge 14 commits into
RhettCreighton wants to merge 14 commits into
Conversation
Add a NATIVE Qt Widgets NFT gallery (no QtWebEngine / WebView / HTML —
QPainter + QListView only), fixture-driven and fully off the money path.
New components (src/):
- nft.h POD NFTItem (name/collection/txid/docHashHex/cachePath/
receivedHeight/isPrivate/verifyState).
- nftgallerymodel.* QAbstractListModel with custom roles and a fingerprint-
guarded setItems() (mirrors TxTableModel's flicker-free
refresh); onImageReady() slot updates matching rows.
- nftgallerydelegate.* QStyledItemDelegate painting a rounded card (dark.qss
tokens) + thumbnail / shimmer placeholder + privacy pill +
verify badge (tinted-SVG mask technique reused from
PrivacyBadgeDelegate); fixed sizeHint(168x208)*DPR.
- nftimagecache.* bounded QThreadPool(4) pipeline: read bytes -> SHA-256 vs
docHashHex -> verifyState; QImageReader decode (scaled-
decode for >4096px / >10MB) -> thumbnail -> atomic PNG
write to AppData/nft_thumbs -> QPixmapCache(128MB). The
worker produces ONLY a QImage; the QPixmap is built on the
GUI thread in deliver() (queued invoke), so QPixmap is
never touched off-thread. In-flight QSet dedupes.
Wiring (surgical, indices kept consistent):
- mainwindow: setupNFTTab() builds a QListView (IconMode/Adjust/uniform/static)
+ delegate + model, inserts a "Collections" tab AFTER Transactions (index 4),
feeds fixtures lazily on first reveal. setupNavRail() adds exactly one
makeRailButton("Collections", <liveIdx>). Gated on getShowNFTGallery().
- rpc.cpp: the zclassicd "Advanced" tab no-double-add sentinel switched from the
index-fragile widget(4)==nullptr to indexOf(zclassicdtab) < 0, so inserting
the gallery at index 4 cannot break the Advanced page.
- settings: getShowNFTGallery()/setShowNFTGallery() (default ON).
Assets/theme:
- application.qrc: 3 verify SVGs (check/x/question, 16px) + 4 sample PNGs.
- dark.qss: NFT page/heading/subhead + borderless gallery view rules (existing
tokens only).
- fixtures: 4 NFTItems — 2 verified, 1 deliberately-wrong-hash MISMATCH (red x),
1 missing-bytes PENDING (amber ?).
Tests: 4 new L0 cases (tst_logic) — fingerprint-guarded no-op refresh, custom
roles + onImageReady verify-state transitions, stable sizeHint, and a full
threaded decode+verify pipeline (verified/mismatch/pending + atomic cache
write). L0 green: 108 passed / 0 failed. Build not run for the full app.
Pure GUI, zero chain dependency, not money-path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gift Native (no web browser) NFT UI for the ZSLP NFT feature (daemon side: ZclassicCommunity/zclassic#131). All rendering + verification is in-app. - Collections tab gallery: NFTGalleryModel + delegate over the existing NFTImageCache/ContentEngine (one engine, posterForToken/posterReady — no second thread pool). Honest per-state cards/tooltips (verified / mismatch / pending / image-not-on-this-computer); flicker-free re-feed; index-off page with a copyable `zslpindex=1` hint; first-run intro shown only while empty. - NFTDetailDialog: large verified image/poster; a verify badge that states ONLY "matches / does NOT match its on-chain fingerprint" (never genuine/official/authorized — disambiguated via What's-This). "Attach the file you have…" affordance: pick a LOCAL file (remote/URL rejected for privacy) -> stream-hash -> MATCH-GATE (cachePut + re-poster only when the computed anchor == the on-chain document_hash) so a RECEIVED NFT can reach the green badge; a non-match shows red and caches nothing; a hash-less NFT is honestly "cannot be verified" (never green). No auto-fetch ever. - NftMintDialog: drag a file -> local fingerprint (never uploaded) -> name -> Create; permanence/public-ledger warning; Public-default (Private disabled coming-soon); visible 0-conf success (Create -> Done, never auto-accept); Cancel/[X] disabled while in flight (no use-after-free). - NFTSendDialog: live recipient validation; shielded address rejected ("private gifts coming soon"); mismatch send-guard (verifyState==2 keeps Send disabled even with a valid t-address — "we won't send a forgery"). - rpc.{cpp,h}: mintNFT / sendNFT / nftProvenance / txReceivedDate wrappers; calm honest daemon-error mapping; test-injection seams (ZCL_WIDGET_TEST only, zero production behavior change). Lifetime-safe (QPointer-guarded async RPC callbacks). C++14. Tests: L0 140, L1 56 (1 pre-existing perf skip). 12 new NFT widget tests — mint gating / remote-drop reject / 0-conf-terminal / closeEvent-in-flight; send 4-state validation + the verifyState==2 mismatch send-guard; detail verified/mismatch badge copy + attach match->verified-and-cached / non-match->red-not-cached. Offscreen bundle delivery gate green. Known follow-ups: Sell/Buy + private-send dialogs (daemon RPCs exist); the "Private — ownership is shielded" pill is an overclaim to remove (ZSLP NFT ownership is always transparent/public); the detail "Send anyway?" mismatch prompt should drop (Send is hard-disabled on mismatch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Native (no web browser) NFT trade UI on top of the daemon nft_* offer RPCs (ZclassicCommunity/zclassic#131), plus the ZcashFoundation#119 honesty fixes so the GUI never overclaims privacy. Sell/Buy: - NFTSellDialog: compose (price ZCL + expiry + buyer t-address) -> nft_makeoffer -> LISTED state with the base64 offer blob (Copy / Save *.znftoffer) + Cancel listing (nft_canceloffer). List is gated on a valid price + buyer address; a mismatch item (verifyState==2) hard-disables List. Public-settlement framing stated plainly (price + both addresses are on-chain). - NFTBuyDialog: paste/open an offer blob -> MANDATORY nft_verifyoffer (crypto + live-UTXO) before Buy is enabled; verify-fail shows an amber reason and keeps Buy disabled; editing after verify resets the gate; Buy -> nft_takeoffer. - rpc.{cpp,h}: makeOffer / verifyOffer / takeOffer / cancelOffer wrappers with calm honest daemon-error mapping; ZCL_WIDGET_TEST test seams (no prod change). - Lifetime-safe: [X]/Cancel swallowed while an RPC is in flight (QPointer + closeEvent guard), mirroring NFTSendDialog. C++14 (empty-QString sentinels). Honesty (ZcashFoundation#119): - nft.h: isPrivate now defaults FALSE (ZSLP NFT ownership is always public). - nftdetaildialog: removed the false "Private - ownership is shielded" pill and dropped the misleading "Send anyway?" mismatch prompt (Send is hard-disabled on a fingerprint mismatch; there is no override). Tests: L0 140/0/0, L1 69 passed / 0 failed / 1 pre-existing perf skip. 9 new NFT widget tests: sell price-gating + ZCL->zat conversion + mismatch-disables- list + success-blob-copy/save/cancel; buy verify-green-enables / verify-fail- amber-disabled / edit-resets-gate / success->Done / close-swallowed-in-flight. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ance GUI build #3 (daemon side: ZclassicCommunity/zclassic data-channel RPCs). Native, no-web-browser private file transfer over the ZDC1 shielded data channel, built on a new shared async-dialog base. Both reviewers approved (0 blockers). L0 155/0, L1 81 (80 pass / 1 pre-existing perf skip). DRY base (audit C-1/C-2/C-4, A-1): - NftAsyncDialog (src/nftasyncdialog.*): single-sources the load-bearing async scaffold — in-flight latch, primary-button relabel/disable, QPointer-guarded completion, Done/Try-again terminal states, and the closeEvent() that swallows [X] while a request is in flight (the prior UAF guard — now impossible to omit). Mint/Send/Sell/Buy refactored onto it; their copy-pasted scaffolds deleted. - nftcommon.*: shared nftValidateTAddrInto() (4-state t-addr validator) + nftPublicTradeNote() (the public-trade honesty sentence) — single-sourced across send/sell/buy so the wording can't drift. - Settings::zatToDecimalString() replaces NFTBuyDialog::humanZcl (bespoke money formatter) and routes through the tested getDecimalString (L0-covered). - rpc.cpp nftListOffers stops sending the dead {"mine":...} wire field. Shield (NATIVE_DISPLAY_UX.md §6, PRIVACY_TECH.md): - rpc.{cpp,h}: sendDataFile / listDataTransfers / getDataTransfer / nftListTransfers wrappers (ZCL_WIDGET_TEST seams); datachannelCalmError maps the default-OFF -32601 to a calm "enable in Settings, then restart" hint. - ShieldSendDialog (inherits NftAsyncDialog): local file -> binary-safe hex, 40000-byte cap enforced up front, own-Sapling From combo, live zs-only recipient validation, MANDATORY permanence consent gating Send, success shows the copyable content fingerprint + per-transfer disclosure key with honest "public ciphertext forever / share the key only with intended readers" framing. - ShieldReceiveDialog: list (honestly session-scoped) + cross-wallet open by transfer_id|fingerprint(+address); verify-before-decrypt; four DISTINCT honest failure states (HASH_MISMATCH/NO_KEY/AEAD_FAIL/INCOMPLETE), never a generic "failed", no fake "try again" on a hard refusal; Save enabled only after verify; binary-safe QByteArray::fromHex (never via QString). - Settings toggle "Enable private file transfers": embedded daemon writes datachannel=1/0 to zclassic.conf + restart prompt; external node persists intent + the calm -32601 path. Honest copy (permanence restated on enable AND disable; disabling cannot delete already-sent files). - Provenance: NFTDetailDialog shows the PUBLIC transfer history (nftListTransfers -> zslp_listtransfers); honest that ownership is always public. - Entry points: Collections tab + detail dialog "Send/Open private file". Honesty: no "ZEC"/"Zcash" in user copy; ownership-always-public stated throughout; "private" scoped to file CONTENT; green = "bytes match fingerprint" only, verified before any decrypt. C++14 (no std::optional/string_view; header-signature includes in headers); SVG image/icon plugins imported. Tests: 14 new cases — shield recipient 4-state validation, file>40000 reject, permanence-consent gate, cross-wallet-by-fingerprint, verify-before-open shows no plaintext on mismatch, [X] swallowed in flight; L0 zatToDecimalString; 2 DRY-drift guards (public-note + validator identical across dialogs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The NFT feature branches are a strict superset of master (which carries the beta7 daemon fixes: -addnode connectivity, SIGBUS, updater); the version label just hadn't been bumped. This is the beta7 line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the two issues hit when running the bundle against a pre-existing node.
BUG 1 — "method not found" on Create. The wallet attaches to an already-running
node and previously ASSUMED it had the bundled NFT feature set; an older
(v2.1.2-beta6) node has no zslp_*/nft_*/z_*datafile RPCs, so mint dead-ended on
a raw -32601 "method not found".
- RPC::probeNFTCapability(): on (re)connect, call zslp_listtokens and classify
ONLY JSON-RPC -32601 as "no NFT support"; success or any other code (e.g. the
index-off -1) => supported; transient/network => fail-open. NOT version-based
(the embedded daemon briefly mis-reported its version yet has the RPCs).
- When unsupported: the Collections tab shows an honest guidance panel (quit any
other running ZClassic node and restart to use the built-in node v2.1.2-beta7+,
or upgrade your node) and the Mint/Sell/Send-private entry points are disabled
with a matching tooltip.
- zslpCalmError() now maps -32601 -> that guidance for EVERY NFT/zslp wrapper
(mint/send/provenance/makeOffer/verifyOffer/takeOffer/cancelOffer/listOffers/
listTransfers); datachannelCalmError() maps it to the guidance on an
unsupported node, else the "enable private file transfers in Settings" hint.
No raw "method not found"/-32601 path remains.
BUG 2 — buttons/dialogs too big for small screens.
- New reusable wrap-to-next-line FlowLayout (src/flowlayout.{h,cpp}); the
Collections heading is on its own line and the 4 action buttons wrap below it,
so they fit any width >= one button (was a non-wrapping QHBoxLayout that
overflowed/clipped on a narrow window). Buttons keep their member pointers so
the capability gating still disables them.
- NFTDetailDialog dropped its hard 760x560 minimum and wraps its body in a
QScrollArea (named "nftDetailScroll"), clamping the open size to fit small
screens.
Tests: L0 136/0, L1 85 pass / 0 fail / 1 pre-existing perf skip.
- Tightened nftDialogs_fitSmallScreen (was tautological — passed on the broken
dialog): asserts minimumSizeHint().width() <= 700, the scroll area exists, and
the dialog actually shrinks to 600x420 with no clipping.
- New nftCalmError_minus32601MapsToGuidanceDirect: asserts zslpCalmError(-32601)
== nftUnsupportedGuidance() verbatim (no "method not found", no -32601, no
ZEC/Zcash) and that a different code does NOT map to the guidance.
GUI repo only; daemon untouched. C++14.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n gettoken fan-out (C-3) - A-1: remove the mineOnly arg from RPC::nftListOffers (decl + def) and the dead `mine` field on the wire (the daemon no longer accepts it). - C-3: refreshNFTs now reads name/ticker/tokenid/documenthash/genesisheight straight off the enriched zslp_listmytokens response (daemon A-3), deleting the Stage-2 per-token zslp_gettoken fan-out — one RPC round-trip per gallery refresh instead of 1+N. No UI field lost. L0 136/0, L1 85/0 (1 pre-existing perf skip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZcashFoundation#125) Whole-app pass from UX_POLISH_PUNCHLIST.md (74 items). Honesty preserved verbatim in meaning everywhere (adversarial honesty review: 0 open blockers). L0 136/L1 85 (5 widget tests updated for the new copy). Don't-make-me-think: - Settings opens on Options, not Troubleshooting; the connection tab is renamed "Advanced" with a one-line "most people never need this" header. - "zclassicd" jargon removed from money screens (no-peers warning, tab title, delete-shielded/restart messages). - Collections entry buttons shortened + unified: Send file / Receive file / Buy / Make; empty state collapsed to one line (honesty moved to WhatsThis). - Mint dialog: 3 stacked paragraphs -> one amber public/permanence line + one grey "file never uploaded" line. Detail view: 11-button cluster tamed (primary Send/Sell + a quiet utility row), one state-aware "Re-check"/"Check my file". - Send privacy badge cue shortened; Settings helper paragraphs -> one line each (data-channel public/permanent honesty kept verbatim); sell expiry is a read-only line, not a 1-choice combo. Honesty fix (code): delete the dead privacyLabel()/privacyColor() delegate helpers that could return "Private"/green, contradicting the always-Public pill (ZcashFoundation#119). Visual consistency: success/verified/valid-address green standardized to #34c759; public = amber #d9822b; red reserved for de-shield/mismatch; honesty fine print bumped off 11px to >=12pt; "collectible" (not "NFT") in visible labels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation#124) Caught by the headless end-to-end test (real bundle, offscreen, real regtest NFTs). Fit any screen (the "too big for my screen" class): - The main window stuck at ~968px and clipped on small screens. Root cause: the Send page's non-wrapping bottom row (fee/totals/Review-&-send/Cancel, horizontalLayout_6) summed its children's widths, and an AdjustToContents scroll area refused to shrink — making Send the widest tab page and pinning the whole QTabWidget's minimum. Fix: re-home that row into a wrapping FlowLayout and let sendToScrollArea shrink (Ignored hpolicy, min 0). Window now fits 820px, honoring the deliberate 648 floor. Verified: winw==screen at 900x600 and 820x560. Honest gallery art (no network, no overclaim): - Cards with no local image bytes paint a deterministic hash-art tile derived from the on-chain fingerprint + a one-line "Tap to add image" affordance — NO download verb, NO fetch (privacy hard rule). Real bytes are resolved OFFLINE from files the user holds or bundled sample resources whose bytes hash to the fingerprint, so a verified (green check = bytes match fingerprint) image renders without leaking. Robustness + test seams (harmless in normal use): - logger: mkpath the log dir so it opens in a fresh profile (was "device not open"). - ZQW_UITEST_TAB=collections deterministically lands the offscreen E2E on Collections. - probeNFTCapability + foreign-node attach now also log their verdict to stderr. Headless E2E proof: real GUI attaches to a regtest NFT node, 2 CLI-minted NFTs render with Public pills + verified checks, buttons fit, no "method not found". L0 136/0, L1 85/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, Browse/Search Three new GUI pillars completing the decentralized-platform user journeys, all headless-tested at L1 (offscreen tst_widget: 122 passed / 1 pre-existing fail / 1 skip; +26 new pillar tests, all green). Names (ZNAM): a Names tab + Register/Resolve/Transfer dialogs + a My-Names list that lazy-loads on tab open (live-E2E fix: it previously only refreshed after a dialog closed). 4 name_* RPC wrappers with a ZCL_WIDGET_TEST seam; target_type as a JSON int; name_listmine called with empty params (daemon self-enumerates the wallet). 11 names_* tests. My-Listings: NFTListingsDialog (view my sell offers, cancel open ones), pure reuse of nft_listoffers/nft_canceloffer; matches role=="sell" literally. 7 myListings_* tests. Browse/Search: NFTMarketDialog + nftBrowseOffers wrapper (nft_browseoffers, RPC_METHOD_NOT_FOUND -> calm), client-side name/collection filter, buy hand-off into the proven NFTBuyDialog via openWithOffer (full znftoffer: blob through the mandatory pre-pay verify gate). 8 browse_* tests. Honest UX: -znamindex/-nftmarket hints; 'discovery off until Tor relay ships' empty state. Coin is ZCL (never ZEC). GUI talks to daemon only via JSON-RPC — no consensus surface. C++14. Pairs with daemon hardening ff46da8fe. Live integrated E2E confirmed full app links + Names/Collections journeys work GUI<->daemon over the real wire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Native NFT Collections UI — gallery, detail/verify, mint-from-file, gift
The native (no web browser) wallet UI for the ZSLP NFT feature. Pairs with the daemon PR ZclassicCommunity/zclassic#131. All rendering and verification happen in-app; nothing is ever auto-fetched.
What's here
NFTImageCache/ContentEngine(one engine; additiveposterForToken/posterReady). Honest per-state cards + tooltips (verified / mismatch / pending / image-not-on-this-computer), flicker-free re-feed, index-off page with a copyablezslpindex=1hint, first-run intro while empty.document_hash); non-match shows red and caches nothing; a hash-less NFT is honestly "cannot be verified."verifyState==2item keeps Send disabled even with a valid t-address).rpc.{cpp,h}mintNFT/sendNFT/nftProvenance/txReceivedDatewith calm honest error mapping andZCL_WIDGET_TEST-only test seams (no production behavior change). Async callbacks areQPointer-guarded (no use-after-free). C++14.Tests
L0 140, L1 56 (1 pre-existing perf skip). 12 new NFT widget tests — mint gating / remote-drop reject / 0-conf-terminal / closeEvent-in-flight; send 4-state validation + the
verifyState==2mismatch send-guard; detail verified/mismatch badge copy + attach match→verified-and-cached / non-match→red-not-cached. Offscreen bundle delivery gate green.Known follow-ups (tracked)
nft_*,z_senddatafile), GUI wrappers pending.run-l0-l1shouldqmakea cleantests/widget/binbeforemake(regeneratesui_settings.h).🤖 Generated with Claude Code