diff --git a/apps/host/index.html b/apps/host/index.html index 4d7c92aa..dfba7d47 100644 --- a/apps/host/index.html +++ b/apps/host/index.html @@ -77,6 +77,7 @@ id="auth-button" class="topbar-btn" title="Login with Polkadot Mobile" + disabled > diff --git a/apps/host/tests/e2e/helpers/extract-qr-payload.ts b/apps/host/tests/e2e/helpers/extract-qr-payload.ts index c8dc9c6e..fb94678d 100644 --- a/apps/host/tests/e2e/helpers/extract-qr-payload.ts +++ b/apps/host/tests/e2e/helpers/extract-qr-payload.ts @@ -11,6 +11,14 @@ export async function extractQrPayload( ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { + const embedded = await page + .locator(canvasSelector) + .getAttribute("data-qr-payload", { timeout: 250 }) + .catch(() => null); + if (embedded?.startsWith("polkadotapp://")) { + return embedded; + } + const px = await page.evaluate((sel) => { const canvas = document.querySelector(sel) as HTMLCanvasElement | null; if (!canvas || canvas.width === 0) return null; diff --git a/bun.lock b/bun.lock index 19807661..51490ec4 100644 --- a/bun.lock +++ b/bun.lock @@ -263,6 +263,7 @@ "version": "0.6.0", "dependencies": { "@dotli/config": "workspace:*", + "@noble/hashes": "^2.2.0", }, "devDependencies": { "@dotli/eslint-config": "workspace:*", diff --git a/packages/content/src/preimage.ts b/packages/content/src/preimage.ts index 7fd652a0..2ebfd9d2 100644 --- a/packages/content/src/preimage.ts +++ b/packages/content/src/preimage.ts @@ -8,6 +8,7 @@ // lookup (convert hash to CID for P2P/IPFS retrieval). import { blake2b } from "@noble/hashes/blake2.js"; +import { fromHex, toHex } from "@dotli/shared/hex"; import { CID } from "multiformats/cid"; import { create } from "multiformats/hashes/digest"; @@ -19,18 +20,13 @@ const RAW_CID_CODEC = 0x55; */ export function computePreimageKey(data: Uint8Array): `0x${string}` { const hash = blake2b(data, { dkLen: 32 }); - return `0x${Array.from(hash, (b: number) => b.toString(16).padStart(2, "0")).join("")}`; + return toHex(hash); } /** * Convert a 0x-prefixed Blake2b-256 hash hex to a CID v1 (raw codec, 0xb220 multihash). */ export function hashToCid(hashHex: string): CID { - const hex = hashHex.startsWith("0x") ? hashHex.slice(2) : hashHex; - const hashBytes = new Uint8Array(hex.length / 2); - for (let i = 0; i < hashBytes.length; i++) { - hashBytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); - } - const digest = create(BLAKE2B_256_MULTIHASH_CODE, hashBytes); + const digest = create(BLAKE2B_256_MULTIHASH_CODE, fromHex(hashHex)); return CID.createV1(RAW_CID_CODEC, digest); } diff --git a/packages/shared/package.json b/packages/shared/package.json index 6ace3004..94728b76 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -22,6 +22,7 @@ "vitest": "^4.1.8" }, "dependencies": { - "@dotli/config": "workspace:*" + "@dotli/config": "workspace:*", + "@noble/hashes": "^2.2.0" } } diff --git a/packages/shared/src/archive-digest.ts b/packages/shared/src/archive-digest.ts index 39bf3f3f..0c8539e9 100644 --- a/packages/shared/src/archive-digest.ts +++ b/packages/shared/src/archive-digest.ts @@ -10,6 +10,8 @@ // storage corruption and passive tampering, not an active same-origin // adversary. +import { toHex } from "./hex"; + /** * Deterministic SHA-256 digest over a file map. For each file (in * sorted-path order) the manifest holds SHA-256(path) ++ SHA-256(bytes): @@ -38,7 +40,5 @@ export async function computeArchiveDigest( manifest.set(fileHash, i * 64 + 32); }); const digest = await crypto.subtle.digest("SHA-256", manifest); - return Array.from(new Uint8Array(digest), (b) => - b.toString(16).padStart(2, "0"), - ).join(""); + return toHex(new Uint8Array(digest)).slice(2); } diff --git a/packages/shared/src/hex.ts b/packages/shared/src/hex.ts new file mode 100644 index 00000000..e5b8e7d8 --- /dev/null +++ b/packages/shared/src/hex.ts @@ -0,0 +1,15 @@ +// Copyright 2026 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: AGPL-3.0-only + +import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; + +export type HexString = `0x${string}`; + +export function toHex(bytes: Uint8Array): HexString { + return `0x${bytesToHex(bytes)}`; +} + +export function fromHex(hex: string): Uint8Array { + const stripped = hex.startsWith("0x") ? hex.slice(2) : hex; + return hexToBytes(stripped); +} diff --git a/packages/truapi-debug/src/format.ts b/packages/truapi-debug/src/format.ts index cf52513e..6a7bc5ef 100644 --- a/packages/truapi-debug/src/format.ts +++ b/packages/truapi-debug/src/format.ts @@ -7,15 +7,14 @@ // suitable for the debug panel. Handles Uint8Array (hex, truncated) and // cycles. No DOM or SDK imports here, kept pure for easy testing. +import { toHex } from "@dotli/shared/hex"; + const MAX_UINT8_PREVIEW_BYTES = 32; const MAX_STRING_PREVIEW_CHARS = 200; function hexOf(bytes: Uint8Array, max: number): string { const preview = bytes.subarray(0, max); - const hex = Array.from(preview, (b) => b.toString(16).padStart(2, "0")).join( - "", - ); - return `0x${hex}${bytes.length > max ? "…" : ""}`; + return `${toHex(preview)}${bytes.length > max ? "…" : ""}`; } function isUint8ArrayLike(v: unknown): v is Uint8Array { diff --git a/packages/truapi-debug/src/styles.css b/packages/truapi-debug/src/styles.css index a65c2e7d..a5368fd0 100644 --- a/packages/truapi-debug/src/styles.css +++ b/packages/truapi-debug/src/styles.css @@ -66,16 +66,6 @@ color: #2dd4bf; border-color: #0f766e; } -.td-layer-attestation { - background: #1a121f; - color: #a78bfa; - border-color: #5b21b6; -} -.td-layer-session { - background: #040e20; - color: #7dd3fc; - border-color: #075985; -} /* System row distinguisher — subtle left-border on rows */ .td-row.system { @@ -90,7 +80,6 @@ --sw-accent: #2dd4bf; } - .td-tooltip { position: absolute; z-index: 1000; @@ -100,8 +89,8 @@ border: 1px solid #3a3a3a; border-radius: 3px; padding: 4px 8px; - font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, - Consolas, monospace; + font-family: + ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace; font-size: 11px; white-space: nowrap; opacity: 0; @@ -121,8 +110,8 @@ color: #d4d4d4; border-top: 1px solid #1f1f1f; z-index: 950; - font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, - Consolas, monospace; + font-family: + ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace; font-size: 12px; display: grid; grid-template-rows: 5px 32px auto 1fr; @@ -423,7 +412,7 @@ } .td-row { display: grid; - grid-template-columns: 72px 16px 90px 58px 1fr; + grid-template-columns: 72px 16px 90px 58px minmax(0, 1fr); align-items: baseline; gap: 8px; padding: 3px 10px; @@ -433,6 +422,12 @@ overflow: hidden; text-overflow: ellipsis; } +.td-row > * { + min-width: 0; +} +.td-row.system { + grid-template-columns: 72px max-content 90px minmax(0, 1fr); +} .td-rid { font-size: 10.5px; font-weight: 600; @@ -483,6 +478,9 @@ overflow: hidden; text-overflow: ellipsis; } +.td-row.system .td-tag-and-summary { + grid-column: auto; +} .td-tag { color: #e0e0e0; } diff --git a/packages/ui/src/styles/topbar.css b/packages/ui/src/styles/topbar.css index b7983c1a..14c83806 100644 --- a/packages/ui/src/styles/topbar.css +++ b/packages/ui/src/styles/topbar.css @@ -308,6 +308,15 @@ border-color: rgba(255, 255, 255, 0.08); color: #fff; } +.topbar-btn:disabled { + cursor: default; + opacity: 0.45; +} +.topbar-btn:disabled:hover { + background: transparent; + border-color: transparent; + color: rgba(255, 255, 255, 0.65); +} .topbar-btn:active { background: rgba(255, 255, 255, 0.11); } diff --git a/packages/ui/src/topbar.ts b/packages/ui/src/topbar.ts index c78dc5fd..37a50e8c 100644 --- a/packages/ui/src/topbar.ts +++ b/packages/ui/src/topbar.ts @@ -15,6 +15,7 @@ import type { Identity } from "@novasamatech/host-papp"; import { log } from "@dotli/shared/log"; import { escapeHtml } from "@dotli/shared/html"; import { isMobileDevice } from "@dotli/shared/device"; +import { toHex } from "@dotli/shared/hex"; import { formatAppVersion, getActiveAppManifest, @@ -172,6 +173,7 @@ export function initTopBar(): void { // Auth button: opens modal (logged out) or popover (logged in) authButton.addEventListener("click", handleAuthButtonClick); + authButton.removeAttribute("disabled"); // Modal close button modalClose.addEventListener("click", closeModal); @@ -362,9 +364,7 @@ function renderLoggedIn(state: AuthState & { status: "authenticated" }): void { username = fullName ?? liteName ?? ""; } else { // Fallback to truncated account address - const id = Array.from(state.session.remoteAccount.accountId) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); + const id = toHex(state.session.remoteAccount.accountId).slice(2); username = `0x${id.slice(0, 6)}...${id.slice(-4)}`; } userPopoverUsername.textContent = username; @@ -381,6 +381,7 @@ function renderPairing(payload: string): void { // Render QR code (lazy-load qrcode lib, guard against stale appends) const canvas = document.createElement("canvas"); + canvas.dataset.qrPayload = payload; const capturedPayload = payload; void import("qrcode") .then((QRCode) =>