From 09b00145e131a35c18803e863c40240d8fad7461 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Fri, 7 Aug 2026 16:13:29 +0100 Subject: [PATCH] fix(payment): stop truncating revert reasons and persist payment errors to a log file A tester's rc.2 failure arrived as a screenshot ending at "reverted with the following signature:" - viem puts the revert selector/reason on a second line of shortMessage, and both shortReason() and paymentErrorSummary() truncated at the first newline. The selector is the only clue to WHY a payment reverts, and it was also captured nowhere: the webview console.error is unreachable in production (no devtools) and Rust tracing wrote to stderr only (discarded for a windowed app). - Flatten newlines in shortReason()/paymentErrorSummary() so the selector (10 chars, e.g. 0x1fb3b5a2) or reason string survives into the status label and toast. - Mirror the full payment error dump (message + stack of the whole cause chain, so the wrapped viem diagnostics survive) into Rust tracing via a new log_frontend_error command. - Give the tracing subscriber a rolling file layer: ~/.config/autonomi/ant-gui/logs/ant-gui..log, daily rotation, 7 files kept, stderr layer unchanged. Falls back to stderr-only if the log dir is unwritable. Co-Authored-By: Claude Fable 5 --- src-tauri/Cargo.lock | 20 +++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 58 +++++++++++++++++++++++++++++++++---- stores/files.ts | 26 +++++++++++++---- tests/utils/payment.test.ts | 19 ++++++++++++ utils/payment.ts | 8 +++-- 6 files changed, 120 insertions(+), 12 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2aedd41..7b85679 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -875,6 +875,7 @@ dependencies = [ "tokio", "toml 0.8.2", "tracing", + "tracing-appender", "tracing-subscriber", "windows-sys 0.59.0", "zip 2.4.2", @@ -7678,6 +7679,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -8675,6 +8682,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 765b767..5dc0e30 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,6 +27,7 @@ toml = "0.8" tauri-plugin-updater = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" ant-core = "0.5.0" evmlib = "0.9" hex = "0.4" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a385f8b..b474bef 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1076,6 +1076,15 @@ fn import_datamaps(src_zip: String) -> Result { + let (writer, guard) = tracing_appender::non_blocking(appender); + // The guard flushes the writer on drop; park it for the app's + // lifetime or nothing is ever written. + static LOG_GUARD: std::sync::OnceLock = + std::sync::OnceLock::new(); + let _ = LOG_GUARD.set(guard); + Some( + tracing_subscriber::fmt::layer() + .with_target(true) + .with_ansi(false) + .with_writer(writer), + ) + } + Err(e) => { + eprintln!("log file unavailable ({e}), stderr only"); + None + } + }; + { + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + let _ = tracing_subscriber::registry() + .with(filter) + .with( + tracing_subscriber::fmt::layer() + .with_target(true) + .with_writer(std::io::stderr), + ) + .with(file_layer) + .try_init(); + } // `mut` is used only under the Windows/Linux single-instance cfg below. #[allow(unused_mut)] @@ -1214,6 +1261,7 @@ pub fn run() { save_upload_history, export_datamaps, import_datamaps, + log_frontend_error, take_pending_deep_links, discover_daemon_url, ensure_daemon_running, diff --git a/stores/files.ts b/stores/files.ts index 31088b3..8f474af 100644 --- a/stores/files.ts +++ b/stores/files.ts @@ -152,19 +152,35 @@ const ACTIVE_STATUSES: FileStatus[] = [ // stalls (they don't need progress bars or spinners in the header). const IN_FLIGHT_STATUSES: FileStatus[] = ['quoting', 'paying', 'uploading', 'downloading'] +/** Full error dump for the log file: message + stack of every layer of the + * cause chain, so the viem diagnostics (revert selector, calldata, request + * args) survive even when wrapped in a plain Error by the preflight. */ +function paymentErrorDump(e: any): string { + const parts: string[] = [] + for (let err = e, depth = 0; err && depth < 5; err = err.cause, depth++) { + parts.push(String(err?.stack ?? err?.message ?? err)) + } + return parts.join('\n--- caused by ---\n') +} + /** One-line summary of a wallet payment error. viem's `.message` is a * multi-line diagnostic dump (request args, calldata, docs link) meant for - * the console, not the UI; `shortMessage` carries the human sentence. A - * 4001 rejection — the code survives WalletConnect and the browser-bridge - * relay alike — gets its own friendlier line. */ + * the log, not the UI; `shortMessage` carries the human sentence — though + * even that puts a revert's selector/reason on a second line, so flatten + * before rendering. A 4001 rejection — the code survives WalletConnect and + * the browser-bridge relay alike — gets its own friendlier line. */ function paymentErrorSummary(e: any): string { - console.error('[payment]', e) // full dump stays available in devtools + console.error('[payment]', e) + // Production builds have no devtools console — mirror the full dump into + // the Rust rolling log so field failures are diagnosable after the fact. + invoke('log_frontend_error', { context: 'payment', detail: paymentErrorDump(e) }).catch(() => {}) const rejected = typeof e?.walk === 'function' ? Boolean(e.walk((c: any) => c?.code === 4001)) : e?.code === 4001 if (rejected) return 'Cancelled in your wallet' - return e?.shortMessage ?? String(e?.message ?? e).split('\n')[0] + const s = e?.shortMessage ?? String(e?.message ?? e).split('\n')[0] + return String(s).replace(/\s*\n\s*/g, ' ').trim() } /** Shape persisted to upload_history.json (kept for backwards compat) */ diff --git a/tests/utils/payment.test.ts b/tests/utils/payment.test.ts index 1081268..18c2e97 100644 --- a/tests/utils/payment.test.ts +++ b/tests/utils/payment.test.ts @@ -125,6 +125,25 @@ describe('payment', () => { expect(writeContract).not.toHaveBeenCalled() }) + it('keeps the revert selector when viem puts it on a second line', async () => { + vi.useFakeTimers() + // viem formats unknown custom errors as "…the following signature:\n0x…" + // — the selector is the only clue to WHY the contract reverts, and + // one-line renderers used to truncate at the newline (field report + // 2026-08-07 arrived as a screenshot ending at the colon). + const revert = Object.assign(new Error('long\nviem\ndump'), { + shortMessage: + 'The contract function "payForMerkleTree" reverted with the following signature:\n0x1fb3b5a2', + }) + estimateContractGas.mockRejectedValue(revert) + + const assertion = expect(payForQuotes({} as any, PAYMENTS)).rejects.toThrow( + 'Payment would fail on-chain: The contract function "payForMerkleTree" reverted with the following signature: 0x1fb3b5a2', + ) + await vi.advanceTimersByTimeAsync(3_000) + await assertion + }) + it('reports a transport failure as an unreachable RPC, not an on-chain verdict', async () => { vi.useFakeTimers() // Shape of a real failure: viem wraps the transport error, keeping it diff --git a/utils/payment.ts b/utils/payment.ts index c261942..8d5224c 100644 --- a/utils/payment.ts +++ b/utils/payment.ts @@ -109,9 +109,13 @@ const PREFLIGHT_ATTEMPTS = 3 const PREFLIGHT_RETRY_DELAY_MS = 1_000 /** One-line failure reason — same field preference the stores use to render - * payment errors (viem's `message` is a multi-line diagnostic dump). */ + * payment errors (viem's `message` is a multi-line diagnostic dump). viem + * puts the payload of a revert — the custom-error selector or reason + * string — on a SECOND line of `shortMessage`; flatten so downstream + * one-line renderers (status label, toast) can't lose it. */ function shortReason(e: any): string { - return e?.shortMessage ?? String(e?.message ?? e).split('\n')[0] + const s = e?.shortMessage ?? String(e?.message ?? e).split('\n')[0] + return String(s).replace(/\s*\n\s*/g, ' ').trim() } /** True when the estimate never reached the chain: the RPC transport failed