Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
58 changes: 53 additions & 5 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,15 @@ fn import_datamaps(src_zip: String) -> Result<datamap_backup::ImportSummary, Str
datamap_backup::import_datamaps(&src_zip)
}

/// Sink for frontend diagnostics that would otherwise die in the webview
/// console — production builds ship without devtools, so errors the frontend
/// only `console.error`s leave no artifact. Routed into tracing so they land
/// in the rolling log file alongside the Rust events.
#[tauri::command]
fn log_frontend_error(context: String, detail: String) {
tracing::error!(target: "ant_gui::frontend", "[{context}] {detail}");
}

pub fn run() {
// WebKitGTK's DMA-BUF renderer (default since 2.42) trips over recent Mesa
// and proprietary NVIDIA drivers, surfacing as "Could not create default
Expand Down Expand Up @@ -1103,11 +1112,49 @@ pub fn run() {
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new("ant_core=info,ant_node=warn,ant_gui=info,warn")
});
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(true)
.with_writer(std::io::stderr)
.try_init();
// Mirror everything to a rolling file as well: production builds have no
// stderr (windowed app) and no devtools, so without this a field failure
// leaves no artifact at all — payment errors in particular used to
// survive only as user screenshots. Daily rotation, 7 files kept.
let file_layer = match tracing_appender::rolling::Builder::new()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("ant-gui")
.filename_suffix("log")
.max_log_files(7)
.build(config::config_path().join("logs"))
{
Ok(appender) => {
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<tracing_appender::non_blocking::WorkerGuard> =
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)]
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 21 additions & 5 deletions stores/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down
19 changes: 19 additions & 0 deletions tests/utils/payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions utils/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down