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
102 changes: 102 additions & 0 deletions typed-doc/copy-to-clipboard-feature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Issue #322: Add copy-to-clipboard on recommended fee
*
* Implements single-click copy of the raw stroop value from the
* RecommendationPanel, with "Copied!" feedback and graceful fallback.
*/

// packages/ui/src/components/dashboard/RecommendationPanel.tsx
import { useState, useCallback } from "react";

interface RecommendationPanelProps {
recommendedFee: number; // raw stroop value (e.g. 3849)
lowFee: number;
mediumFee: number;
highFee: number;
}

export function RecommendationPanel({
recommendedFee,
lowFee,
mediumFee,
highFee,
}: RecommendationPanelProps) {
const [copied, setCopied] = useState(false);

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(String(recommendedFee));
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// clipboard API unavailable — fallback
const ta = document.createElement("textarea");
ta.value = String(recommendedFee);
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
}, [recommendedFee]);

return (
<div className="rounded-lg border p-4 space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">
Recommended Fee
</h3>

<div className="flex items-center gap-3">
<span className="text-3xl font-bold tabular-nums">
{recommendedFee.toLocaleString()}
</span>
<span className="text-sm text-muted-foreground">stroops</span>

<button
onClick={handleCopy}
className="ml-auto inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent"
aria-label={copied ? "Copied" : "Copy fee value"}
>
{copied ? (
<>
<CheckIcon className="h-4 w-4 text-green-500" />
Copied!
</>
) : (
<>
<CopyIcon className="h-4 w-4" />
Copy
</>
)}
</button>
</div>

<div className="grid grid-cols-3 gap-2 text-xs text-muted-foreground">
<div>Low: {lowFee.toLocaleString()}</div>
<div>Medium: {mediumFee.toLocaleString()}</div>
<div>High: {highFee.toLocaleString()}</div>
</div>
</div>
);
}

// Icons (inline to avoid import complexity)
function CopyIcon({ className }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
);
}

function CheckIcon({ className }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}
127 changes: 127 additions & 0 deletions typed-doc/simulation-instrumentation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Issue #340: Add span instrumentation to simulation module
*
* Wraps all public simulation functions with tracing spans using
* #[tracing::instrument] for observability of the fee simulation pipeline.
*
* Requirements:
* - Each span includes: function name, input config fields, output sample count
* - Spans visible in both JSON and human log formats
*/

// packages/devkit/src/simulation/fee_model.rs
use tracing::instrument;

/// Parameters controlling fee model behaviour.
#[derive(Debug)]
pub struct FeeModelConfig {
pub min_fee: u64,
pub max_fee: u64,
pub base_reserve: u64,
pub ledger_capacity: u64,
}

/// Output from the fee computation.
pub struct FeeEstimate {
pub recommended: u64,
pub low: u64,
pub medium: u64,
pub high: u64,
pub sample_count: usize,
}

impl FeeModel {
/// Compute fee estimates from the current ledger state.
#[instrument(skip(self), fields(config = ?self.config))]
pub fn estimate(&self, ledger: &LedgerState) -> FeeEstimate {
let samples = self.collect_samples(ledger);
let sorted = self.sort_samples(samples);

let estimate = FeeEstimate {
recommended: sorted.percentile(50),
low: sorted.percentile(10),
medium: sorted.percentile(25),
high: sorted.percentile(75),
sample_count: sorted.len(),
};

tracing::info!(
sample_count = estimate.sample_count,
recommended = estimate.recommended,
"fee estimate computed"
);

estimate
}
}

// packages/devkit/src/simulation/network_load.rs
use tracing::instrument;

impl NetworkLoad {
/// Estimate the current network congestion level as a fraction 0.0 … 1.0.
#[instrument(skip(self))]
pub fn estimate_current_load(&self, recent_txs: &[Transaction]) -> f64 {
if recent_txs.is_empty() {
tracing::warn!("no recent transactions to estimate load");
return 0.0;
}

let load = self.compute_load(recent_txs);
let clamped = load.clamp(0.0, 1.0);

tracing::debug!(
raw_load = load,
clamped_load = clamped,
tx_count = recent_txs.len(),
"network load estimated"
);

clamped
}
}

// packages/devkit/src/simulation/congestion_predictor.rs
use tracing::instrument;

pub enum CongestionLevel {
Low,
Medium,
High,
Extreme,
}

impl CongestionPredictor {
/// Predict short-term congestion based on recent and historical data.
#[instrument(skip(self, recent_loads), fields(window = recent_loads.len()))]
pub fn predict_congestion_short_term(
&self,
recent_loads: &[f64],
historical_avg: f64,
) -> CongestionLevel {
let current_avg = recent_loads.iter().copied().sum::<f64>()
/ recent_loads.len().max(1) as f64;

tracing::debug!(
current_avg = current_avg,
historical_avg = historical_avg,
"congestion prediction computed"
);

if current_avg > historical_avg * 1.5 {
CongestionLevel::High
} else if current_avg > historical_avg * 1.2 {
CongestionLevel::Medium
} else {
CongestionLevel::Low
}
}

/// Predict long-term congestion trend.
#[instrument(skip(self))]
pub fn predict_congestion_trend(&self, loads: &[f64]) -> f64 {
let trend = self.compute_trend(loads);
tracing::info!(trend = trend, "long-term congestion trend");
trend
}
}
60 changes: 60 additions & 0 deletions typed-doc/simulation-test-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Issue #338: Test plan — fee recommendation & monitoring modules
*
* Tests cover the simulation pipeline, fee recommendation output,
* structured logging, and clipboard interaction.
*/

// ── 1. Fee Simulation Tests ───────────────────────────────────────────────────
//
// Test fee_model.rs
// ✓ compute_base_fee returns correct value for given ledger data
// ✓ compute_base_fee handles empty / missing ledger entries
// ✓ compute_base_fee clamps result to [MIN_FEE, MAX_FEE]
//
// Test network_load.rs
// ✓ estimate_current_load returns 0.0 … 1.0 range
// ✓ estimate_current_load handles no recent transactions gracefully
// ✓ load multiplier correctly inflates base fee
//
// Test congestion_predictor.rs
// ✓ predict_congestion returns enum variant for each load band
// ✓ predict_congestion_short_term matches historical pattern
// ✓ predict_congestion integrates with tracing span

// ── 2. Fee Recommendation Output Tests ────────────────────────────────────────
//
// Test RecommendationPanel rendering
// ✓ displays recommendedFee, lowFee, mediumFee, highFee
// ✓ formats large numbers with toLocaleString separators
//
// Test clipboard interaction
// ✓ click copies raw integer value (not formatted string)
// ✓ shows "Copied!" label for 1.5 s after click
// ✓ reverts to "Copy" after timeout
// ✓ uses navigator.clipboard.writeText when available
// ✓ falls back to document.execCommand("copy") when clipboard API unavailable
// ✓ does not throw when clipboard write is rejected

// ── 3. Structured Logging Tests ──────────────────────────────────────────────
//
// Test logging.rs
// ✓ init_logger respects DEVKIT_LOG env var levels (trace/debug/info/warn/error)
// ✓ JSON output format when stdout is not a TTY
// ✓ human-readable format when stdout is a TTY
// ✓ subscriber captures tracing events and spans
// ✓ subscriber does not panic on malformed level strings

// ── 4. Integration Tests ─────────────────────────────────────────────────────
//
// Test end-to-end fee recommendation
// ✓ simulation → fee calculation → panel render → copy
// ✓ structured log output contains expected fields
// ✓ tracing spans are present in JSON log output

// ── Test File Locations ───────────────────────────────────────────────────────
//
// packages/core/src/simulation/ fee_model.rs, network_load.rs, congestion_predictor.rs
// packages/core/src/monitoring/ logging.rs
// packages/ui/src/components/ RecommendationPanel.tsx
// packages/ui/src/__tests__/ RecommendationPanel.test.tsx
83 changes: 83 additions & 0 deletions typed-doc/structured-logging-module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Issue #339: Add structured logging module
*
* Implements a tracing-based structured logger for the devkit.
*
* Requirements:
* - Log level configurable via DEVKIT_LOG env var
* - JSON output for machine readability
* - Human-readable output for local dev (auto-detect TTY)
*/

// packages/devkit/src/monitoring/logging.rs
use std::env;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use tracing_subscriber::fmt::format::FmtSpan;

/// Initialise the global tracing subscriber.
///
/// Behaviour:
/// - Reads `DEVKIT_LOG` env var (default: "info")
/// - Auto-detects TTY: human-readable in terminals, JSON elsewhere
/// - Captures span open/close events for accurate timing
pub fn init_logger() {
let log_level = env::var("DEVKIT_LOG").unwrap_or_else(|_| "info".into());
let is_tty = atty::is(atty::Stream::Stdout);

let env_filter = EnvFilter::try_new(&log_level)
.unwrap_or_else(|_| EnvFilter::new("info"));

let fmt_layer = if is_tty {
// Human-friendly output with ANSI colours
fmt::layer()
.pretty()
.with_line_number(true)
.with_target(true)
.with_span_events(FmtSpan::CLOSE)
.boxed()
} else {
// JSON output for machine consumption
fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_line_number(true)
.with_file(true)
.boxed()
};

tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
}

#[cfg(test)]
mod tests {
use super::*;
use tracing::info;

#[test]
fn test_default_level_is_info() {
// When DEVKIT_LOG is unset, info!() should produce output
// but debug!() should not
init_logger();
info!("this should appear");
}

#[test]
fn test_env_var_override() {
env::set_var("DEVKIT_LOG", "debug");
init_logger();
// Both info!() and debug!() should produce output now
}
}

// packages/devkit/src/monitoring/mod.rs
pub mod logging;

// packages/devkit/Cargo.toml additions:
// [dependencies]
// tracing = "0.1"
// tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
// atty = "0.2"
Loading