diff --git a/.gitignore b/.gitignore index 77f47b6..a15ef19 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ datashards/ docs/* !docs/ARCHITECTURE.md !docs/DESIGN-SYSTEM-RULEBOOK.md +!docs/LINUX-BUILD.md # Compiled binaries (should be built, not committed) TELA-Browser-Wails diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac358b2..cc94e0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,40 +33,52 @@ Thank you for your interest in contributing to HOLOGRAM — a native desktop DER ### Prerequisites -- **Go** 1.24.0+ +- **Go** 1.24.0+ — install from [go.dev/dl](https://go.dev/dl). Distro packages (`apt install golang-go`, etc.) are usually too old; HOLOGRAM will not build on Go 1.22. - **Wails v2 CLI:** `go install github.com/wailsapp/wails/v2/cmd/wails@latest` - **Node.js** 18+ ### Linux users +All current distros (Ubuntu 24.04+, Debian 13, Fedora 40+, Arch) ship `webkit2gtk-4.1` (libsoup3). The Makefile auto-applies `-tags webkit2_41` on Linux — install the matching system packages: + ```bash # Ubuntu/Debian -sudo apt install libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.0-dev +sudo apt install libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.1-dev # Fedora sudo dnf install gtk3-devel glib2-devel webkit2gtk4.1-devel # Arch Linux -sudo pacman -S gtk3 glib2 webkit2gtk +sudo pacman -S gtk3 glib2 webkit2gtk-4.1 ``` +> Build error, runtime crash, or vite timeout on Linux? See **[docs/LINUX-BUILD.md](docs/LINUX-BUILD.md)** — covers the libsoup conflict, OOM during `make all`, and stale dev-server cleanup. + ### Run in development mode ```bash git clone https://github.com/DHEBP/HOLOGRAM.git cd HOLOGRAM cd frontend && npm install && cd .. + +# macOS / Windows wails dev + +# Linux +wails dev -tags webkit2_41 +# or, equivalently: +make dev ``` ### Build ```bash -# Full build (HOLOGRAM + derod + simulator) +# Full build (HOLOGRAM + derod + simulator) — auto-tags on Linux make all # HOLOGRAM only -wails build +wails build # macOS / Windows +wails build -tags webkit2_41 # Linux ``` ### Run Go tests diff --git a/Makefile b/Makefile index bb07656..de33950 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,18 @@ else SIMULATOR_BIN = simulator-linux-$(GOARCH) endif +# Linux: link against webkit2gtk-4.1 (libsoup3) instead of the default +# webkit2gtk-4.0 (libsoup2). All current distros (Ubuntu 24.04+, Debian 13, +# Fedora 40+, Arch) ship 4.1 only — building without this tag fails to link +# or crashes at runtime due to libsoup2 ↔ libsoup3 conflicts. +# Override on the command line if you really need the legacy 4.0 binding: +# make WAILS_TAGS= ... +ifeq ($(GOOS),linux) + WAILS_TAGS ?= -tags webkit2_41 +else + WAILS_TAGS ?= +endif + # Build directories BUILD_DIR = build/bin DEROHE_PKG = github.com/deroproject/derohe @@ -65,13 +77,13 @@ endif # Build HOLOGRAM using wails (dev/local build with metadata) hologram: @echo "🔨 Building HOLOGRAM ($(VERSION), $(COMMIT))..." - wails build -ldflags "$(LDFLAGS)" + wails build $(WAILS_TAGS) -ldflags "$(LDFLAGS)" @echo "✅ HOLOGRAM built" # Release build — clean, trimpath, metadata injected (use this for distribution) release: derod simulator @echo "🚀 Building HOLOGRAM release ($(VERSION), $(COMMIT))..." - wails build -ldflags "$(LDFLAGS)" -clean -trimpath + wails build $(WAILS_TAGS) -ldflags "$(LDFLAGS)" -clean -trimpath @echo "✅ Release build complete: $(BUILD_DIR)/$(HOLOGRAM_BIN)" # Build derod from derohe source @@ -125,7 +137,7 @@ test-mtp-integration: mtp-anchor # Development mode dev: - wails dev + wails dev $(WAILS_TAGS) # Clean build artifacts clean: diff --git a/README.md b/README.md index da29754..9ca636e 100644 --- a/README.md +++ b/README.md @@ -44,24 +44,26 @@ Browse TELA applications. Manage your DERO. Build and deploy dApps with an integ ### Prerequisites -- **Go** 1.24.0+ +- **Go** 1.24.0+ — install from [go.dev/dl](https://go.dev/dl) (distro packages like `apt install golang-go` are usually too old) - **Wails** v2 CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` - **Node.js** 18+ #### Linux-specific Dependencies +All current Linux distros (Ubuntu 24.04+, Debian 13, Fedora 40+, Arch) have moved to `webkit2gtk-4.1` (libsoup3). The Makefile auto-applies the matching `-tags webkit2_41` build tag on Linux — you just need the right system packages: + ```bash # Ubuntu/Debian -sudo apt install libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.0-dev +sudo apt install libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.1-dev # Fedora sudo dnf install gtk3-devel glib2-devel webkit2gtk4.1-devel # Arch Linux -sudo pacman -S gtk3 glib2 webkit2gtk +sudo pacman -S gtk3 glib2 webkit2gtk-4.1 ``` -> **Note:** Ubuntu 24.04/Debian 13 users may need additional steps for webkit2gtk-4.0. See the [DERO community Linux setup guide](https://github.com/deroproject/documentation) or open an issue if you hit a platform-specific snag. +> Hitting a build error, runtime crash, or vite timeout on Linux? See [docs/LINUX-BUILD.md](docs/LINUX-BUILD.md) — it covers the libsoup conflict, the `webkit2_41` tag, OOM during `make all`, and stale dev-server cleanup. ### Development @@ -69,7 +71,14 @@ sudo pacman -S gtk3 glib2 webkit2gtk git clone https://github.com/DHEBP/HOLOGRAM.git cd HOLOGRAM cd frontend && npm install && cd .. + +# macOS / Windows wails dev + +# Linux +wails dev -tags webkit2_41 +# or, equivalently: +make dev ``` ### Production Build (Recommended) @@ -93,9 +102,12 @@ This builds the DERO daemon and simulator directly from the derohe source code, If you prefer to download derod separately: ```bash -# Build HOLOGRAM only +# macOS / Windows wails build +# Linux +wails build -tags webkit2_41 + # Output locations: # macOS: build/bin/Hologram.app # Linux: build/bin/Hologram @@ -105,10 +117,10 @@ wails build ### Cross-Platform Builds ```bash -wails build -platform darwin/amd64 # macOS Intel -wails build -platform darwin/arm64 # macOS Apple Silicon -wails build -platform linux/amd64 # Linux x64 -wails build -platform windows/amd64 # Windows x64 +wails build -platform darwin/amd64 # macOS Intel +wails build -platform darwin/arm64 # macOS Apple Silicon +wails build -platform linux/amd64 -tags webkit2_41 # Linux x64 +wails build -platform windows/amd64 # Windows x64 ``` --- diff --git a/app.go b/app.go index ca95801..a48a559 100644 --- a/app.go +++ b/app.go @@ -834,6 +834,45 @@ func (a *App) DaemonGetSC(scid string) map[string]interface{} { if err != nil { return ErrorResponse(err) } + res = normalizeDEROGetSCResult(res) + return map[string]interface{}{"success": true, "result": res} +} + +// GetSCVariable queries specific keys from a smart contract +// Used for fetching individual variables like avatar data without fetching all contract data +func (a *App) GetSCVariable(scid string, keys []string) map[string]interface{} { + if a.daemonClient == nil { + return map[string]interface{}{"success": false, "error": "Not connected to any node. Please connect to a network first."} + } + if scid == "" { + return map[string]interface{}{"success": false, "error": "SCID is required"} + } + if len(keys) == 0 { + return map[string]interface{}{"success": false, "error": "At least one key is required"} + } + + params := map[string]interface{}{ + "scid": scid, + "code": false, + "variables": false, + "keysstring": keys, + } + + res, err := a.daemonClient.Call("DERO.GetSC", params) + if err != nil { + a.logToConsole(fmt.Sprintf("[ERR] GetSCVariable failed for %s: %v", scid[:16], err)) + return ErrorResponse(err) + } + + // Extract valuesstring from result + if resMap, ok := res.(map[string]interface{}); ok { + return map[string]interface{}{ + "success": true, + "scid": scid, + "valuesstring": resMap["valuesstring"], + } + } + return map[string]interface{}{"success": true, "result": res} } diff --git a/blockchain.go b/blockchain.go index e1fc5fd..1e705a5 100644 --- a/blockchain.go +++ b/blockchain.go @@ -156,6 +156,8 @@ func (a *App) FetchTELAContent(scid string) (*TELAContent, error) { if docData == nil { continue // Skip failed fetches } + // Add SCID to docData so processDOC can access it + docData["scid"] = docSCIDs[i] if err := a.processDOC(docData, content); err != nil { a.logToConsole(fmt.Sprintf(" [WARN] Failed to process DOC %d: %v", i+1, err)) failedDOCs = append(failedDOCs, i) @@ -170,7 +172,7 @@ func (a *App) FetchTELAContent(scid string) (*TELAContent, error) { for _, i := range failedDOCs { scid := docSCIDs[i] - a.logToConsole(fmt.Sprintf(" [RETRY] Re-fetching DOC %d: %s...", i+1, scid[:16])) + a.logToConsole(fmt.Sprintf(" [RETRY] Re-fetching DOC %d: %s...", i+1, truncateSCID(scid, 16))) // Re-fetch the DOC data, err := a.fetchSmartContract(scid, true, true) @@ -179,6 +181,9 @@ func (a *App) FetchTELAContent(scid string) (*TELAContent, error) { continue } + // Add SCID to data so processDOC can access it + data["scid"] = scid + // Try to process again if err := a.processDOC(data, content); err != nil { a.logToConsole(fmt.Sprintf(" [ERR] Retry process failed for DOC %d: %v", i+1, err)) @@ -198,7 +203,7 @@ func (a *App) FetchTELAContent(scid string) (*TELAContent, error) { // Check if we got at least HTML if content.HTML == "" { // Attempt shard assembly if this is a shard index dURL - if du, ok := content.Meta["durl"].(string); ok && isShardIndexDURL(du) { + if du, ok := content.Meta["durl"].(string); ok && isEmbeddedShardsINDEX(du) { a.logToConsole("[LINK] Shard index detected; assembling shard files") assembled := assembleShardFiles(content) if assembled != "" { @@ -243,12 +248,31 @@ func (a *App) FetchTELAContent(scid string) (*TELAContent, error) { return content, nil } -// isShardIndexDURL returns true if the dURL ends with .tela.shards +// isShardIndexDURL returns true if the dURL ends with .tela.shards (embedded shard INDEX) func isShardIndexDURL(durl string) bool { s := strings.ToLower(strings.TrimSpace(durl)) return strings.HasSuffix(s, ".tela.shards") } +// isEmbeddedINDEX returns true if the contract is an INDEX (not a DOC) +// by checking for telaVersion or DOC1 keys in stringkeys +func isEmbeddedINDEX(stringKeys map[string]interface{}) bool { + if stringKeys == nil { + return false + } + _, hasTelaVersion := stringKeys["telaVersion"] + _, hasDOC1 := stringKeys["DOC1"] + return hasTelaVersion || hasDOC1 +} + +// truncateSCID safely truncates a SCID for logging (prevents slice bounds panic) +func truncateSCID(scid string, maxLen int) string { + if len(scid) <= maxLen { + return scid + } + return scid[:maxLen] +} + // isShardChunkName returns true if the filename matches the shard naming pattern // (e.g., "img.png-1.gz", "rive.js-3.gz") produced by expandFileToShards. var shardChunkRe = regexp.MustCompile(`^(.+)-(\d+)(\.\w+)$`) @@ -345,12 +369,19 @@ func (a *App) reassembleShardChunks(content *TELAContent) { } } -// isLibraryDURL returns true if the dURL ends with .tela.lib +// isLibraryDURL returns true if the dURL ends with .tela.lib (TELA library INDEX) func isLibraryDURL(durl string) bool { s := strings.ToLower(strings.TrimSpace(durl)) return strings.HasSuffix(s, ".tela.lib") } +// isEmbeddedShardsINDEX returns true if the dURL ends with .shards (embedded shard INDEX) +// This is used for detecting embedded INDEX contracts like rive.wasm-2.35.3.shards +func isEmbeddedShardsINDEX(durl string) bool { + s := strings.ToLower(strings.TrimSpace(durl)) + return strings.HasSuffix(s, ".shards") +} + // fetchSingleDOC renders a standalone DOC contract directly (not part of an INDEX) // This is used when a user pastes a DOC SCID directly into the browser func (a *App) fetchSingleDOC(scid string, docData map[string]interface{}) (*TELAContent, error) { @@ -915,6 +946,29 @@ func (a *App) processDOC(docData map[string]interface{}, content *TELAContent) e } } + // Check if this is actually an embedded INDEX (not a DOC) + // Embedded INDEXes have telaVersion or DOC1 keys but no docType + if isEmbeddedINDEX(stringKeys) { + dURL := fallbackMeta.DURL + if durlHex, ok := stringKeys["dURL"].(string); ok { + dURL = decodeHexString(durlHex) + } + + // Handle embedded .shards INDEX (e.g., rive.js-2.35.3.shards) + if isEmbeddedShardsINDEX(dURL) { + scid := "" + if scidStr, ok := docData["scid"].(string); ok { + scid = scidStr + } + a.logToConsole(fmt.Sprintf(" [EMBED] Detected embedded .shards INDEX: %s (%s...)", dURL, truncateSCID(scid, 16))) + return a.processEmbeddedShardsINDEX(docData, content) + } + + // For other embedded INDEXes (.lib), we can add support later + a.logToConsole(fmt.Sprintf(" [WARN] Embedded INDEX detected but not .shards: %s (skipping)", dURL)) + return nil + } + // Get docType (hex-encoded) - with nil check docType := fallbackMeta.DocType if docTypeHex, ok := stringKeys["docType"].(string); ok { @@ -1041,6 +1095,172 @@ func (a *App) processDOC(docData map[string]interface{}, content *TELAContent) e return nil } +// processEmbeddedShardsINDEX handles an embedded INDEX with .shards dURL +// This is used for sharded libraries like rive.js that are split across multiple DOC contracts +// The function fetches all DOCs from the embedded INDEX, concatenates them, and decompresses +func (a *App) processEmbeddedShardsINDEX(indexData map[string]interface{}, content *TELAContent) error { + // Get the embedded INDEX's dURL to derive the output filename + stringKeys, _ := indexData["stringkeys"].(map[string]interface{}) + code, _ := indexData["code"].(string) + fallbackMeta := extractDOCMetadataFromCode(code) + + dURL := fallbackMeta.DURL + if durlHex, ok := stringKeys["dURL"].(string); ok { + dURL = decodeHexString(durlHex) + } + + // For HTTP serving, the browser requests the full path: /rive.wasm-2.35.3.shards/rive.wasm + // - Directory name: dURL (e.g., "rive.wasm-2.35.3.shards") + // - Filename: base name without version (e.g., "rive.wasm") + // We need BOTH: + // - Full path for HTTP serving: "rive.wasm-2.35.3.shards/rive.wasm" + // - Simple name for HTML inlining: "rive.wasm" + + // Get base name by stripping .shards and version + baseName := strings.TrimSuffix(dURL, ".shards") + versionRe := regexp.MustCompile(`^(.+)-\d+\.\d+\.\d+$`) + if m := versionRe.FindStringSubmatch(baseName); len(m) > 1 { + baseName = m[1] + } + + // Full path for HTTP serving: "dURL/baseName" + fullPath := dURL + "/" + baseName + + // We'll store under both names + outputName := baseName + + a.logToConsole(fmt.Sprintf(" [EMBED] Processing embedded shards: %s -> %s (HTTP: %s)", dURL, outputName, fullPath)) + + // Extract DOC SCIDs from the embedded INDEX + docSCIDs := extractDOCsSCIDs(indexData) + if len(docSCIDs) == 0 { + return fmt.Errorf("no DOC SCIDs found in embedded INDEX %s", dURL) + } + + a.logToConsole(fmt.Sprintf(" [EMBED] Found %d shard DOCs in %s", len(docSCIDs), dURL)) + + // Fetch all DOC contracts and extract their content in order + var shardContents []string + var compression string + + for i, docSCID := range docSCIDs { + docData, err := a.fetchSmartContract(docSCID, true, false) + if err != nil { + a.logToConsole(fmt.Sprintf(" [EMBED] Failed to fetch shard DOC %d (%s...): %v", i+1, truncateSCID(docSCID, 16), err)) + continue + } + + docCode, ok := docData["code"].(string) + if !ok || docCode == "" { + a.logToConsole(fmt.Sprintf(" [EMBED] Shard DOC %d has no code", i+1)) + continue + } + + // Extract shard content from the DOC's comment block + shardContent := extractFileContentFromCode(docCode) + if shardContent == "" { + a.logToConsole(fmt.Sprintf(" [EMBED] Failed to extract content from shard DOC %d", i+1)) + continue + } + + // Detect compression from first shard's filename + if i == 0 { + docStringKeys, _ := docData["stringkeys"].(map[string]interface{}) + docMeta := extractDOCMetadataFromCode(docCode) + docFileName := docMeta.FileName + if fnHex, ok := docStringKeys["var_header_name"].(string); ok { + docFileName = decodeHexString(fnHex) + } else if fnHex, ok := docStringKeys["nameHdr"].(string); ok { + docFileName = decodeHexString(fnHex) + } + if strings.HasSuffix(docFileName, ".gz") { + compression = ".gz" + } + } + + shardContents = append(shardContents, shardContent) + } + + if len(shardContents) == 0 { + return fmt.Errorf("no shard content extracted from embedded INDEX %s", dURL) + } + + a.logToConsole(fmt.Sprintf(" [EMBED] Extracted %d shard chunks for %s", len(shardContents), outputName)) + + // Concatenate all shard contents + concatenated := strings.Join(shardContents, "") + + // Decompress if shards were compressed + var finalContent string + if compression == ".gz" { + decompressed, err := decompressGzip(concatenated) + if err != nil { + a.logToConsole(fmt.Sprintf(" [EMBED] Decompression failed for %s: %v", outputName, err)) + return fmt.Errorf("failed to decompress shards for %s: %w", outputName, err) + } + finalContent = decompressed + a.logToConsole(fmt.Sprintf(" [EMBED] Decompressed %s: %d bytes -> %d bytes", outputName, len(concatenated), len(finalContent))) + } else { + finalContent = concatenated + a.logToConsole(fmt.Sprintf(" [EMBED] Assembled %s: %d bytes (uncompressed)", outputName, len(finalContent))) + } + + // Determine docType based on file extension + docType := "TELA-STATIC-1" + ext := strings.ToLower(filepath.Ext(outputName)) + switch ext { + case ".js": + docType = "TELA-JS-1" + case ".css": + docType = "TELA-CSS-1" + case ".html", ".htm": + docType = "TELA-HTML-1" + case ".wasm": + docType = "TELA-STATIC-1" + } + + // Store by type (similar to regular DOC processing) + // Store under BOTH: + // - Simple name (for HTML inlining): "rive.wasm" + // - Full path (for HTTP serving): "rive.wasm-2.35.3.shards/rive.wasm" + switch { + case strings.HasPrefix(docType, "TELA-JS"): + content.JS = append(content.JS, finalContent) + if content.JSByName == nil { + content.JSByName = make(map[string]string) + } + content.JSByName[outputName] = finalContent + content.JSByName[fullPath] = finalContent + a.logToConsole(fmt.Sprintf(" [EMBED] Stored %s as JS (%d bytes)", outputName, len(finalContent))) + + case strings.HasPrefix(docType, "TELA-CSS"): + content.CSS = append(content.CSS, finalContent) + if content.CSSByName == nil { + content.CSSByName = make(map[string]string) + } + content.CSSByName[outputName] = finalContent + content.CSSByName[fullPath] = finalContent + a.logToConsole(fmt.Sprintf(" [EMBED] Stored %s as CSS (%d bytes)", outputName, len(finalContent))) + + default: + if content.StaticByName == nil { + content.StaticByName = make(map[string]string) + } + content.StaticByName[outputName] = finalContent + content.StaticByName[fullPath] = finalContent + a.logToConsole(fmt.Sprintf(" [EMBED] Stored %s as static (%d bytes)", outputName, len(finalContent))) + } + + // Record raw file + content.Files = append(content.Files, DocFile{ + Name: outputName, + Content: finalContent, + DocType: docType, + }) + + return nil +} + // extractFileContentFromCode extracts file content from smart contract code // The actual file content is in a comment block (/* ... */) func extractFileContentFromCode(code string) string { diff --git a/docs/LINUX-BUILD.md b/docs/LINUX-BUILD.md new file mode 100644 index 0000000..2354f9d --- /dev/null +++ b/docs/LINUX-BUILD.md @@ -0,0 +1,203 @@ +# Linux Build Guide + +HOLOGRAM is built with [Wails v2](https://wails.io/), which renders the UI through the system's WebKitGTK runtime. The Linux WebKit ecosystem went through a transition starting around 2024 that affects every Wails app, not just HOLOGRAM. This guide covers the gotchas you will hit on a modern distro and how to resolve each one. + +If you just want the working command and don't care about the why: + +```bash +# Ubuntu / Debian +sudo apt install libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.1-dev + +# Fedora +sudo dnf install gtk3-devel glib2-devel webkit2gtk4.1-devel + +# Arch +sudo pacman -S gtk3 glib2 webkit2gtk-4.1 + +# Build / run +make all # full build (auto-applies -tags webkit2_41 on Linux) +wails dev -tags webkit2_41 # dev mode +wails build -tags webkit2_41 # HOLOGRAM-only build +``` + +Read on for the *why*, plus fixes for the four most common Linux failure modes. + +--- + +## The libsoup2 → libsoup3 split (the big one) + +Wails v2 has two WebKit bindings: + +| Binding | WebKit package | HTTP backend | +|---------|---------------|--------------| +| Default (no tag) | `webkit2gtk-4.0` | libsoup **2** | +| `-tags webkit2_41` | `webkit2gtk-4.1` | libsoup **3** | + +Every current Linux distro has dropped or is dropping the libsoup2-based `webkit2gtk-4.0`: + +- **Ubuntu 24.04+ / Debian 13+** — package renamed to `libwebkit2gtk-4.1-dev`, the 4.0 package is gone. +- **Fedora 40+** — only ships `webkit2gtk4.1-devel`. +- **Arch** — ships only `webkit2gtk-4.1`. The bare `webkit2gtk` package is no longer in repos. + +If you build HOLOGRAM **without** `-tags webkit2_41` on these systems you'll see one of: + +- Linker errors about missing `webkit2gtk-4.0` / pkg-config can't find it. +- A successful build that **crashes at startup** because libsoup2 and libsoup3 get loaded into the same process. This is a known WebKitGTK runtime conflict, not a HOLOGRAM bug. + +**Fix:** install the 4.1 dev package above and pass the build tag (or use `make`, which sets it for you). + +> ### Wails v3 / `webkitgtk-6.0`? +> [wails#3193](https://github.com/wailsapp/wails/issues/3193) tracks moving Wails to GTK4 / WebKitGTK 6.0 in the v3 branch. That is **not** required for HOLOGRAM to build — `webkit2_41` is the supported path on Wails v2 and works on every modern distro. + +--- + +## Failure 1 — `pkg-config: command not found` or "Package webkit2gtk-4.0 was not found" + +You're missing the dev headers, or you have the 4.0 package name in muscle memory. + +```bash +# Ubuntu/Debian +sudo apt install pkg-config libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.1-dev + +# Fedora +sudo dnf install pkgconf gtk3-devel glib2-devel webkit2gtk4.1-devel + +# Arch +sudo pacman -S pkgconf gtk3 glib2 webkit2gtk-4.1 +``` + +Then rebuild with `-tags webkit2_41` (or just `make`). + +--- + +## Failure 2 — Build succeeds but the app crashes / hangs at launch + +Symptom: `wails build` finishes, but `./build/bin/Hologram` immediately segfaults, freezes the desktop, or prints something about libsoup before dying. + +This is the **libsoup2 + libsoup3 in the same process** conflict. It happens when: + +- You built without `-tags webkit2_41` but the runtime found a libsoup3-only WebKitGTK, +- Or the system has both libsoup major versions installed and a transitive dep pulled in the wrong one. + +**Fix:** + +```bash +make clean +make all # auto-tags on Linux +# Or explicitly: +wails build -tags webkit2_41 +``` + +--- + +## Failure 3 — `make all` hangs the machine / triggers OOM killer + +`make all` builds three things end-to-end: + +1. `derod` (DERO daemon, from `derohe` source) +2. `simulator` (DERO simulator) +3. HOLOGRAM (Wails Go + bundled Svelte frontend, CGO-linked against WebKit) + +The Go linker for the third step is memory-heavy, and parallel compilation can spike well past 8 GB on a fresh checkout. On a small VM or laptop this can OOM-kill X/Wayland and lock the desktop. + +**Mitigations, easiest first:** + +```bash +# 1. Limit Go's parallelism (4 → 2 workers) +GOFLAGS='-p=2' make all + +# 2. Skip derod/simulator entirely — build HOLOGRAM only +wails build -tags webkit2_41 + +# 3. If derod is the heavy step, build sequentially with reduced concurrency +make derod GOFLAGS='-p=1' +make simulator GOFLAGS='-p=1' +make hologram + +# 4. Add swap if you don't have any +sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile +sudo mkswap /swapfile && sudo swapon /swapfile +``` + +If the machine is locking up *before* the link stage, it's almost certainly the frontend `vite` build chewing memory while Go's compiler runs in parallel. `GOFLAGS='-p=2'` is the cleanest fix. + +--- + +## Failure 4 — `wails dev` errors with "Timed out waiting for Vite to output a URL" + +``` +failed to find Vite server URL: Timed out waiting for Vite to output a URL after 10 seconds +``` + +A previous `wails dev` session left a vite/wails process holding port `5173` (or another dev port). The new session can't bind it and gives up. + +**Fix:** + +```bash +lsof -ti:5173 | xargs -r kill -9 +pkill -f vite +pkill -f wails +wails dev -tags webkit2_41 +``` + +If it keeps recurring, `npm run dev` may be running standalone in another terminal — close it. + +--- + +## Failure 5 — Go is too old (`go version go1.22.x linux/amd64`) + +HOLOGRAM requires Go **1.24.0+**. Most distro packages (`apt install golang-go`, etc.) are pinned to whatever shipped with the distro release and are usually one or two majors behind. + +**Install a current Go from upstream:** + +```bash +# Remove any distro Go first +sudo apt remove golang-go golang 2>/dev/null || true +sudo rm -rf /usr/local/go + +# Pick the latest from https://go.dev/dl/ +GO_VERSION=1.24.2 +wget https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz +sudo tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz + +# Add Go to PATH (persist across sessions) +echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.profile +source ~/.profile + +go version # should print 1.24.x or newer +``` + +Then reinstall the Wails CLI under the new Go: + +```bash +go install github.com/wailsapp/wails/v2/cmd/wails@latest +export PATH=$PATH:$HOME/go/bin +wails doctor +``` + +`wails doctor` is worth running once on a fresh machine — it sanity-checks Go, Node, npm, pkg-config, and WebKit headers in one shot. + +--- + +## TL;DR cheat sheet + +```bash +# 1. Up-to-date Go (>= 1.24) from go.dev, not from your distro +go version + +# 2. Wails CLI +go install github.com/wailsapp/wails/v2/cmd/wails@latest +export PATH=$PATH:$HOME/go/bin + +# 3. WebKit 4.1 dev headers (your distro's package name) +sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libglib2.0-dev # Debian/Ubuntu +sudo dnf install webkit2gtk4.1-devel gtk3-devel glib2-devel # Fedora +sudo pacman -S webkit2gtk-4.1 gtk3 glib2 # Arch + +# 4. Build with the right tag (or use make, which does this automatically) +make dev # interactive +make all # full build +wails build -tags webkit2_41 # HOLOGRAM only +``` + +Anything else broken? Open a [GitHub Discussion](https://github.com/DHEBP/HOLOGRAM/discussions) with your distro, `go version`, `wails doctor` output, and the exact error. diff --git a/error_messages.go b/error_messages.go index e4f5063..1d4d41a 100644 --- a/error_messages.go +++ b/error_messages.go @@ -91,9 +91,9 @@ var TELADeploymentErrors = []TELADeploymentError{ // === Wallet/Account errors === { Pattern: `Account Unregistered|account unregistered|-32098.*unregistered`, - Title: "Destination wallet is not registered", - Description: "The destination address for this transaction is not registered on the blockchain. In DERO, addresses must be registered before receiving transactions.", - Fix: "Use a registered wallet address. In simulator mode, use one of the pre-seeded test wallets.", + Title: "Recipient wallet is not registered", + Description: "The recipient's wallet address is not yet registered on the DERO blockchain. In DERO, addresses must be registered before they can receive transactions.", + Fix: "The recipient needs to register their wallet first. They can do this by clicking 'Register Now' in their wallet app (Backup & Security section). Registration uses PoW and can take a few minutes. In simulator mode, use one of the pre-seeded test wallets which are already registered.", Example: "", }, { @@ -350,7 +350,7 @@ var UserFriendlyErrors = map[string]string{ "wallet file not found": "Wallet file not found. Check the path.", "insufficient balance": "Not enough DERO for this transaction.", "insufficient funds": "Not enough DERO for this transaction.", - "account unregistered": "Destination wallet is not registered on the blockchain.", + "account unregistered": "Recipient's wallet is not registered on-chain. They need to open their wallet and click 'Register Now' (in Backup & Security) before they can receive DERO.", "sending to self": "Cannot send transactions to your own address.", // Transaction errors @@ -384,7 +384,7 @@ var UserFriendlyErrors = map[string]string{ "-32601": "RPC method not found. Check method name.", "-32700": "Invalid JSON in request.", "-32602": "Invalid parameters for RPC method.", - "-32098": "DERO daemon error. Check the error details.", + "-32098": "Recipient's wallet is not registered on-chain. They need to open their wallet and click 'Register Now' (in Backup & Security) before they can receive DERO.", // Gnomon errors "gnomon not running": "Gnomon indexer is not running. Start it in Settings.", diff --git a/explorer_service.go b/explorer_service.go index fca7685..6f31e49 100644 --- a/explorer_service.go +++ b/explorer_service.go @@ -312,9 +312,9 @@ func (a *App) GetCoinbaseMiner(txid string) map[string]interface{} { if txHex == "" { return map[string]interface{}{ - "success": true, + "success": true, "isCoinbase": false, - "message": "No raw transaction hex available", + "message": "No raw transaction hex available", } } @@ -547,7 +547,7 @@ func (a *App) SearchAddress(address string) map[string]interface{} { if a.gnomonClient.IsRunning() { // Search through Gnomon for SCIDs owned by this address ownedSCIDs := a.getOwnedSCIDs(address) - + return map[string]interface{}{ "success": true, "address": address, @@ -910,7 +910,7 @@ func (a *App) GetBlockchainStats() map[string]interface{} { // Extract relevant stats - info is already map[string]interface{} stats := map[string]interface{}{} - + stats["height"] = info["height"] stats["topoheight"] = info["topoheight"] stats["difficulty"] = info["difficulty"] @@ -1192,7 +1192,7 @@ func (a *App) GetMempoolExtended(maxCount int) map[string]interface{} { func (a *App) GetSCInfo(scid string) map[string]interface{} { // Normalize SCID to lowercase (DERO requires lowercase hex) normalizedSCID := strings.ToLower(strings.TrimSpace(scid)) - + a.logToConsole(fmt.Sprintf("[...] Getting SC info: %s", normalizedSCID[:16]+"...")) params := map[string]interface{}{ @@ -1205,6 +1205,7 @@ func (a *App) GetSCInfo(scid string) map[string]interface{} { if err != nil { return ErrorResponse(err) } + result = normalizeDEROGetSCResult(result) scData := map[string]interface{}{} if resultMap, ok := result.(map[string]interface{}); ok { @@ -1248,7 +1249,7 @@ func (a *App) GetRingMembers(txid string) map[string]interface{} { "index": idx, "members": []string{}, } - + if addresses, ok := payload.([]interface{}); ok { members := []string{} for _, addr := range addresses { @@ -1258,12 +1259,12 @@ func (a *App) GetRingMembers(txid string) map[string]interface{} { } payloadRing["members"] = members payloadRing["count"] = len(members) - + if len(members) > ringSize { ringSize = len(members) } } - + ringData = append(ringData, payloadRing) } } @@ -1274,10 +1275,10 @@ func (a *App) GetRingMembers(txid string) map[string]interface{} { a.logToConsole(fmt.Sprintf("[OK] Found %d ring groups, max size %d", len(ringData), ringSize)) return map[string]interface{}{ - "success": true, - "txid": txid, - "rings": ringData, - "ringCount": len(ringData), + "success": true, + "txid": txid, + "rings": ringData, + "ringCount": len(ringData), "maxRingSize": ringSize, } } @@ -1294,15 +1295,14 @@ func (a *App) GetTransactionWithRings(txid string) map[string]interface{} { // Get ring members ringResult := a.GetRingMembers(txid) - + // Combine results return map[string]interface{}{ - "success": true, - "txid": txid, - "tx": txResult["tx"], - "rings": ringResult["rings"], - "ringCount": ringResult["ringCount"], + "success": true, + "txid": txid, + "tx": txResult["tx"], + "rings": ringResult["rings"], + "ringCount": ringResult["ringCount"], "maxRingSize": ringResult["maxRingSize"], } } - diff --git a/file_service.go b/file_service.go index fc0110a..b7a5392 100644 --- a/file_service.go +++ b/file_service.go @@ -551,6 +551,159 @@ func (a *App) SelectFile() string { return selection } +// SelectFileWithContent opens a native file picker dialog and returns the file content as base64. +// This is used by TELA dApps that need to select files (e.g., importing images). +// The accept parameter is a comma-separated list of MIME types (e.g., "image/png,image/jpeg") +// or file extensions (e.g., ".png,.jpg"). If empty, all files are shown. +func (a *App) SelectFileWithContent(title string, accept string) map[string]interface{} { + a.logToConsole(fmt.Sprintf("[FILE] SelectFileWithContent: title=%s, accept=%s", title, accept)) + + // Build file filters from accept parameter + filters := []runtime.FileFilter{} + if accept != "" { + // Parse accept string to build filters + parts := strings.Split(accept, ",") + patterns := []string{} + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, ".") { + // Extension like ".png" + patterns = append(patterns, "*"+part) + } else if strings.Contains(part, "/") { + // MIME type like "image/png" + switch part { + case "image/png": + patterns = append(patterns, "*.png") + case "image/jpeg": + patterns = append(patterns, "*.jpg", "*.jpeg") + case "image/gif": + patterns = append(patterns, "*.gif") + case "image/svg+xml": + patterns = append(patterns, "*.svg") + case "image/*": + patterns = append(patterns, "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.webp") + case "text/plain": + patterns = append(patterns, "*.txt") + case "text/html": + patterns = append(patterns, "*.html", "*.htm") + case "text/css": + patterns = append(patterns, "*.css") + case "application/javascript", "text/javascript": + patterns = append(patterns, "*.js") + case "application/json": + patterns = append(patterns, "*.json") + } + } + } + if len(patterns) > 0 { + filters = append(filters, runtime.FileFilter{ + DisplayName: "Allowed Files", + Pattern: strings.Join(patterns, ";"), + }) + } + } + // Always add "All Files" as fallback + filters = append(filters, runtime.FileFilter{ + DisplayName: "All Files", + Pattern: "*.*", + }) + + dialogTitle := title + if dialogTitle == "" { + dialogTitle = "Select File" + } + + selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: dialogTitle, + Filters: filters, + }) + if err != nil { + a.logToConsole(fmt.Sprintf("[ERR] SelectFileWithContent: Dialog error - %v", err)) + return map[string]interface{}{ + "success": false, + "error": fmt.Sprintf("File dialog error: %v", err), + } + } + + // User cancelled + if selection == "" { + a.logToConsole("[FILE] SelectFileWithContent: User cancelled") + return map[string]interface{}{ + "success": false, + "cancelled": true, + } + } + + // Read the file + info, err := os.Stat(selection) + if err != nil { + a.logToConsole(fmt.Sprintf("[ERR] SelectFileWithContent: Stat error - %v", err)) + return map[string]interface{}{ + "success": false, + "error": fmt.Sprintf("Cannot read file: %v", err), + } + } + + // Limit file size to 50MB for safety + const maxSize = 50 * 1024 * 1024 + if info.Size() > maxSize { + a.logToConsole(fmt.Sprintf("[ERR] SelectFileWithContent: File too large (%d bytes)", info.Size())) + return map[string]interface{}{ + "success": false, + "error": fmt.Sprintf("File too large (%d bytes). Maximum is 50MB.", info.Size()), + } + } + + data, err := os.ReadFile(selection) + if err != nil { + a.logToConsole(fmt.Sprintf("[ERR] SelectFileWithContent: Read error - %v", err)) + return map[string]interface{}{ + "success": false, + "error": fmt.Sprintf("Failed to read file: %v", err), + } + } + + // Detect MIME type from extension + ext := strings.ToLower(filepath.Ext(selection)) + mimeType := "application/octet-stream" + switch ext { + case ".png": + mimeType = "image/png" + case ".jpg", ".jpeg": + mimeType = "image/jpeg" + case ".gif": + mimeType = "image/gif" + case ".svg": + mimeType = "image/svg+xml" + case ".webp": + mimeType = "image/webp" + case ".txt": + mimeType = "text/plain" + case ".html", ".htm": + mimeType = "text/html" + case ".css": + mimeType = "text/css" + case ".js": + mimeType = "application/javascript" + case ".json": + mimeType = "application/json" + } + + // Encode as base64 + base64Data := base64.StdEncoding.EncodeToString(data) + + a.logToConsole(fmt.Sprintf("[OK] SelectFileWithContent: Selected %s (%d bytes)", info.Name(), info.Size())) + + return map[string]interface{}{ + "success": true, + "filename": info.Name(), + "path": selection, + "size": info.Size(), + "mimeType": mimeType, + "base64": base64Data, + } +} + // ReadTextFile reads a text file and returns its content. // Used by the Deploy SC flow to load .bas files from disk. func (a *App) ReadTextFile(filePath string) map[string]interface{} { diff --git a/frontend/src/lib/components/BatchUpload.svelte b/frontend/src/lib/components/BatchUpload.svelte index 80daa04..ca7bf73 100644 --- a/frontend/src/lib/components/BatchUpload.svelte +++ b/frontend/src/lib/components/BatchUpload.svelte @@ -1355,7 +1355,7 @@ {iconValidation.message}

+ {:else} +

Recommended: use an on-chain icon DOC SCID (100x100 SVG/PNG works well).

{/if} diff --git a/frontend/src/lib/components/RevealSecretModal.svelte b/frontend/src/lib/components/RevealSecretModal.svelte new file mode 100644 index 0000000..82e5a32 --- /dev/null +++ b/frontend/src/lib/components/RevealSecretModal.svelte @@ -0,0 +1,578 @@ + + + + +{#if show} + +{/if} + + diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index 955c543..36bdc9b 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -1,6 +1,6 @@ @@ -1711,7 +1685,7 @@ TX broadcast successfully. Waiting for blockchain confirmation... {:else} New Wallet - Your address isn't on-chain yet. Receive DERO to auto-register, or register manually via PoW. + Your address isn't on-chain yet. Click Register Now to complete on-chain registration via PoW before receiving DERO. {/if} @@ -2847,83 +2821,22 @@ RECOVERY SEED - +
- {#if !seedRevealed} - -
- - Enter your wallet password to view your recovery seed phrase -
- -
- - -
- - {#if backupError} -
- - {backupError} -
- {/if} - -
- -
- {:else} - -
- -

Your Recovery Seed

-

Write down these 25 words in order. This is the ONLY way to recover your wallet.

-
- -
- {#each revealedSeed.split(' ') as word, i} -
- {i + 1} - {word} -
- {/each} -
- -
-
- - NEVER share your seed with anyone -
-
- - Hologram will NEVER ask for your seed -
-
- - Store this offline in a safe place -
-
- -
- - -
- {/if} +
+ + Your password is required every time the seed is revealed. The seed is held only while open and is auto-hidden after 60 seconds. +
+ +
+ +
@@ -2935,97 +2848,29 @@ WALLET KEYS - +
- {#if !keysRevealed} - -
- - Enter your wallet password to view your secret and public keys -
- -
- -
- CRITICAL: Your secret key provides full control over your wallet. Never share it with anyone. -
-
- -
- - -
- - {#if keysError} -
- - {keysError} -
- {/if} - -
- -
- {:else} - -
- -
-
- SECRET KEY - CRITICAL -
-
- {revealedSecretKey} -
- -
- - This key provides full wallet control. Keep it secure and never share it. -
-
- - -
- - -
-
- PUBLIC KEY -
-
- {revealedPublicKey} -
- -
- Public key can be shared safely. It's used to verify signatures. -
-
-
- -
- +
+ + Your password is required every time keys are revealed. Keys are held only while open and are auto-hidden after 60 seconds. +
+ +
+ +
+ CRITICAL: Your secret key provides full control over your wallet. Never share it with anyone.
- {/if} +
+ +
+ +
@@ -3211,7 +3056,7 @@ {/each} - @@ -3546,6 +3391,60 @@ on:close={() => { editingContact = null; }} /> + + + + +{#if showClearWalletsConfirm} + +{/if} +