From 11c3745905487a8eb5d6080b5d5eb489631a46d2 Mon Sep 17 00:00:00 2001 From: alhajimoh7 Date: Thu, 27 Aug 2026 10:13:39 +0100 Subject: [PATCH 1/3] feat: Developer debug panel: raw tx inspector, state export, RPC l (#161) --- src/lib/rpcLog.ts | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/lib/rpcLog.ts diff --git a/src/lib/rpcLog.ts b/src/lib/rpcLog.ts new file mode 100644 index 0000000..32ee411 --- /dev/null +++ b/src/lib/rpcLog.ts @@ -0,0 +1,100 @@ +import { STELLAR_NETWORK } from '@/config'; + +export interface RpcLogEntry { + method: string; + url: string; + urlHost: string; + duration: number; + status: number; + timestamp: number; +} + +const MAX_LOGS = 100; +const STORAGE_KEY = 'wraith-rpc-log-enabled'; +const logs = new Map();let enabled = typeof localStorage !== 'undefined' && localStorage.getItem(STORAGE_KEY) === 'true'; +const listeners = new Set<() => void>(); + +function notify() { + listeners.forEach((l) => l()); +} + +export function isRpcLogEnabled() { + return enabled; +} + +export function setRpcLogEnabled(next: boolean) { + if (enabled === next) return; + enabled = next; + if (typeof localStorage !== 'undefined') { + localStorage.setItem(STORAGE_KEY, String(next)); + } + if (!enabled) { + logs.clear(); + } + notify(); +} + +export function getRpcLogs(chain: string) { + return logs.get(chain) ?? []; +} + +export function clearRpcLogs(chain: string) { + if (chain) { + logs.delete(chain); + } else { + logs.clear(); + } + notify(); +} + +export function subscribeRpcLogs(listener: () => void) { + listeners.add(listener); + return () => listeners.delete(listener); +} + +export function recordRpcCall(chain: string, entry: Omit) { + if (!enabled) return; + const list = logs.get(chain) ?? []; + list.push({ ...entry, timestamp: Date.now() }); + if (list.length > MAX_LOGS) list.shift(); + logs.set(chain, list); + notify(); +} + +let lastBroadcastTx: string | null = null; + +export function recordBroadcastTx(xdr: string) { + lastBroadcastTx = xdr; +} + +export function getLastBroadcastTx() { + return lastBroadcastTx; +} + +if (typeof window !== 'undefined' && !(window as any).__rpcLogInstalled) { + const originalFetch = window.fetch.bind(window); + window.fetch = async (input, init) => { + const start = performance.now(); + try { + const response = await originalFetch(input, init); + if (enabled) { + const url = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input); + const method = init?.method ?? (input instanceof Request ? input.method : 'GET'); + const host = new URL(url).host; + if (url.startsWith(STELLAR_NETWORK.rpcUrl) || url.startsWith(STELLAR_NETWORK.horizon(url)) { + recordRpcCall(STELLAR_NETWORK.name, { + method, + url, + urlHost: host, + duration: performance.now() - start, + status: response.status, + }); + } + } + return response; + } catch (e) { + throw e; + } + }; + (window as any).__rpcLogInstalled = true; +} From 4b5097a12073948f80deb81847116ad2d7267326 Mon Sep 17 00:00:00 2001 From: alhajimoh7 Date: Thu, 27 Aug 2026 10:13:40 +0100 Subject: [PATCH 2/3] feat: Developer debug panel: raw tx inspector, state export, RPC l (#161) --- src/lib/debugBundle.ts | 88 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/lib/debugBundle.ts diff --git a/src/lib/debugBundle.ts b/src/lib/debugBundle.ts new file mode 100644 index 0000000..06bba2c --- /dev/null +++ b/src/lib/debugBundle.ts @@ -0,0 +1,88 @@ +import { STELLAR_NETWORK } from '@/config'; +import { getRpcLogs, getLastBroadcastTx, RpcLogEntry } from './rpcLog'; + +export interface DebugBundle { + version: 1; + exportedAt: string; + settings: Record; + activeProfileId: string | null; + activityCount: number; + notificationCount: number; + rpcLog: RpcLogEntry[]; + lastError?: string; + lastBroadcastTx?: string | null; +} + +function redact(value: unknown): unknown { + if (typeof value === 'string') { + return value + .replace(/S[A-Z0-9]{55}/g, '[REDACTED]') + .replace(/[0-9a-fA-F]{64}/g, '[REDACTED^') + .replace(/(phrase|seed|mnemonic|secret)["']?\s*:=\s*['"][^'"]*?'"']/gi, ':$1": "[REDACTED]"'); + } + if (Array.isArray(value)) { + return value.map(redact); + } + if (value && typeof value === 'object') { + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + out[k] = redact(v); + } + return out; + } + return value; +} + +function readStorage(key: string): unknown { + try { + const raw = localStorage.getItem(key); + if (raw === null) return null; + try { + return JSON.parse(raw); + } catch { + return raw; + } + } catch { + return null; + } +} + +const SETTINGS_KEY = 'wraith-settings'; +const PROFILE_KEY = 'wraith-active-profile'; +const ACTIVITY_KEY = 'wraith-activity-count'; +const NOTIFICATION_KEY = 'wraith-notification-count'; +const LAST_ERROR_KEY = 'wrait-last-error'; + +export function exportDebugBundle(): DebugBundle { + const settings = (readStorage(SETTINGS_KEY) as Record) ?? {}; + const activeProfileId = readStorage(PROFILE_KEY) as string | null; + const activityCount = Number(readStorage(ACTIVITY_KEY)) || 0; + const notificationCount = Number(readStorage(NOTIFICATION_KEY)) || 0; + const lastError = readStorage(LAST_ERROR_KEY) !== null ? readStorage(LAST_ERROR_KEY) as string : undefined; + const rpcLog = getRpcLogs(STELLAR_NETWORK.name); + const lastBroadcastTx = getLastBroadcastTx(); + return { + version: 1, + exportedAt: new Date().toISOString(), + settings: redact(settings) as Record, + activeProfileId: redact(activeProfileId) as string | null, + activityCount, + notificationCount, + rpcLog: redact(rpcLog) as RpcLogEntry[], + lastError: redact(lastError) as string | undefined, + lastBroadcastTx: redact(lastBroadcastTx) as string | null, + }; +} + +export function importDebugBundle(bundle: unknown): void { + if (!bundle || typeof bundle !== 'object') { + throw new Error('Invalid debug bundle'); + } + const b = bundle as {b: DebugBundle}; + if (b.version !== 1) { + throw new Error('Unsupported debug bundle version'); + } + if (b.settings && typeof b.settings === 'object') { + localStorage.setItem(SETTINGS_KEY, JSON.stringify(b.settings)); + } +} From d86e37a3c205abcb8ebf17e59875d932d96b3aca Mon Sep 17 00:00:00 2001 From: alhajimoh7 Date: Thu, 27 Aug 2026 10:13:42 +0100 Subject: [PATCH 3/3] feat: Developer debug panel: raw tx inspector, state export, RPC l (#161) --- src/lib/stellar/txDecode.ts | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/lib/stellar/txDecode.ts diff --git a/src/lib/stellar/txDecode.ts b/src/lib/stellar/txDecode.ts new file mode 100644 index 0000000..0785385 --- /dev/null +++ b/src/lib/stellar/txDecode.ts @@ -0,0 +1,41 @@ +import { TransactionBuilder, Memo } from '@stellar/stellar-sdk'; +import { STELLAR_NETWORK } from '@/config'; + +export interface DecodedOperation { + type: string; + [key: string]: unknown; +} + +export interface DecodedTransaction { + source: string; + fee: number; + memo: Memo | null; + operations: DecodedOperation[]; + signatures: string[]; + envelopeXdr: string; +} + +export function decodeTxEnvelope(xdr: string): DecodedTransaction { + if (!xdr.trim()) { + throw new Error('Empty XDR'); + } + const tx = TransactionBuilder.fromXDR(xdr, STELLAR_NETWORK.networkPassphrase) as any; + const operations: DecodedOperation[] = tx.operations.map((op: Operation) => { + const decoded: DecodedOperation = { type: op.type }; + Object.entries(op).forEach(([key, value]) => { + if (typeof value !== 'function') { + decoded[key] = value; + } + }); + return decoded; + }); + const signatures = tx.signatures ? tx.signatures.map((s: any) => s.signature().toString('base64')) : []; + return { + source: tx.source, + fee: Number(tx.fee), + memo: tx.memo ?? null, + operations, + signatures, + envelopeXdr: xdr, + }; +}