use std::sync::Arc;
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use tokio::sync::{mpsc, watch};
use super::Component;
use crate::action::Action;
use crate::api::ApiClient;
use crate::theme;
use crate::watch::StampsSnapshot;
use bee::postage::{BatchBucket, PostageBatch, PostageBatchBuckets};
use bee::swarm::BatchId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StampStatus {
Pending,
Expired,
Critical,
Skewed,
Healthy,
}
impl StampStatus {
fn color(self) -> Color {
let t = theme::active();
match self {
Self::Pending => t.info,
Self::Expired => t.fail,
Self::Critical => t.fail,
Self::Skewed => t.warn,
Self::Healthy => t.pass,
}
}
fn label(self) -> &'static str {
match self {
Self::Pending => "⏳ pending",
Self::Expired => "✗ expired",
Self::Critical => "✗ critical",
Self::Skewed => "⚠ skewed",
Self::Healthy => "✓",
}
}
}
#[derive(Debug, Clone)]
pub struct StampRow {
pub label: String,
pub batch_id_short: String,
pub volume: String,
pub worst_bucket_pct: u32,
pub worst_bucket_raw: String,
pub ttl: String,
pub immutable: bool,
pub status: StampStatus,
pub why: Option<String>,
}
pub const FILL_BIN_LABELS: &[&str] = &[
"0 %",
"1 – 19 %",
"20 – 49 %",
"50 – 79 %",
"80 – 99 %",
"100 %",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StampDrillView {
pub depth: u8,
pub bucket_depth: u8,
pub upper_bound: u32,
pub total_chunks: u64,
pub theoretical_capacity: u128,
pub fill_distribution: [u32; 6],
pub worst_buckets: Vec<WorstBucket>,
pub worst_pct: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WorstBucket {
pub bucket_id: u32,
pub collisions: u32,
pub pct: u32,
}
#[derive(Debug, Clone)]
pub enum DrillState {
Idle,
Loading {
batch_id: BatchId,
},
Loaded {
batch_id: BatchId,
view: StampDrillView,
},
Failed {
batch_id: BatchId,
error: String,
},
}
type DrillFetchResult = (BatchId, std::result::Result<PostageBatchBuckets, String>);
pub struct Stamps {
client: Arc<ApiClient>,
rx: watch::Receiver<StampsSnapshot>,
snapshot: StampsSnapshot,
selected: usize,
scroll_offset: usize,
drill: DrillState,
fetch_tx: mpsc::UnboundedSender<DrillFetchResult>,
fetch_rx: mpsc::UnboundedReceiver<DrillFetchResult>,
}
impl Stamps {
pub fn new(client: Arc<ApiClient>, rx: watch::Receiver<StampsSnapshot>) -> Self {
let snapshot = rx.borrow().clone();
let (fetch_tx, fetch_rx) = mpsc::unbounded_channel();
Self {
client,
rx,
snapshot,
selected: 0,
scroll_offset: 0,
drill: DrillState::Idle,
fetch_tx,
fetch_rx,
}
}
fn pull_latest(&mut self) {
self.snapshot = self.rx.borrow().clone();
let n = self.snapshot.batches.len();
if n == 0 {
self.selected = 0;
} else if self.selected >= n {
self.selected = n - 1;
}
}
fn drain_fetches(&mut self) {
while let Ok((batch_id, result)) = self.fetch_rx.try_recv() {
match &self.drill {
DrillState::Loading { batch_id: pending } if *pending == batch_id => {}
_ => continue, }
self.drill = match result {
Ok(buckets) => DrillState::Loaded {
batch_id,
view: Self::compute_drill_view(&buckets),
},
Err(error) => DrillState::Failed { batch_id, error },
};
}
}
pub fn rows_for(snap: &StampsSnapshot) -> Vec<StampRow> {
snap.batches.iter().map(row_from_batch).collect()
}
pub fn compute_drill_view(buckets: &PostageBatchBuckets) -> StampDrillView {
let upper_bound = buckets.bucket_upper_bound.max(1);
let mut fill_distribution = [0u32; 6];
let mut total_chunks: u64 = 0;
for b in &buckets.buckets {
total_chunks += u64::from(b.collisions);
let bin = bucket_fill_bin(b.collisions, upper_bound);
fill_distribution[bin] += 1;
}
let mut sorted: Vec<&BatchBucket> = buckets.buckets.iter().collect();
sorted.sort_by(|a, b| {
b.collisions
.cmp(&a.collisions)
.then_with(|| a.bucket_id.cmp(&b.bucket_id))
});
let worst_buckets: Vec<WorstBucket> = sorted
.iter()
.take(10)
.map(|b| WorstBucket {
bucket_id: b.bucket_id,
collisions: b.collisions,
pct: pct_of(b.collisions, upper_bound),
})
.collect();
let worst_pct = worst_buckets.first().map(|w| w.pct).unwrap_or(0);
let theoretical_capacity = (1u128 << buckets.bucket_depth) * u128::from(upper_bound);
StampDrillView {
depth: buckets.depth,
bucket_depth: buckets.bucket_depth,
upper_bound,
total_chunks,
theoretical_capacity,
fill_distribution,
worst_buckets,
worst_pct,
}
}
fn maybe_start_drill(&mut self) {
if self.snapshot.batches.is_empty() {
return;
}
let i = self.selected.min(self.snapshot.batches.len() - 1);
let batch_id = self.snapshot.batches[i].batch_id;
if let DrillState::Loading { batch_id: pending } = &self.drill {
if *pending == batch_id {
return; }
}
let client = self.client.clone();
let tx = self.fetch_tx.clone();
tokio::spawn(async move {
let res = client
.bee()
.postage()
.get_postage_batch_buckets(&batch_id)
.await
.map_err(|e| e.to_string());
let _ = tx.send((batch_id, res));
});
self.drill = DrillState::Loading { batch_id };
}
}
fn bucket_fill_bin(collisions: u32, upper_bound: u32) -> usize {
if collisions == 0 {
return 0;
}
if collisions >= upper_bound {
return 5; }
let pct = pct_of(collisions, upper_bound);
match pct {
0 => 0, 1..=19 => 1,
20..=49 => 2,
50..=79 => 3,
80..=99 => 4,
_ => 5,
}
}
fn pct_of(collisions: u32, upper_bound: u32) -> u32 {
if upper_bound == 0 {
return 0;
}
let pct = (u64::from(collisions) * 100) / u64::from(upper_bound);
pct.min(100) as u32
}
fn row_from_batch(b: &PostageBatch) -> StampRow {
let label = if b.label.is_empty() {
"(unlabeled)".to_string()
} else {
b.label.clone()
};
let batch_hex = b.batch_id.to_hex();
let batch_id_short = if batch_hex.len() > 8 {
format!("{}…", &batch_hex[..8])
} else {
batch_hex
};
let theoretical_bytes: u128 = (1u128 << b.depth) * 4096;
let volume = format_bytes(theoretical_bytes);
let worst_bucket_pct = worst_bucket_pct(b);
let upper_bound = 1u32 << b.depth.saturating_sub(b.bucket_depth);
let worst_bucket_raw = format!("{}/{}", b.utilization, upper_bound);
let ttl = format_ttl_seconds(b.batch_ttl);
let (status, why) = if !b.usable {
(
StampStatus::Pending,
Some("waiting on chain confirmation (~10 blocks).".into()),
)
} else if b.batch_ttl <= 0 {
(
StampStatus::Expired,
Some("paid balance exhausted; topup or stop using.".into()),
)
} else if worst_bucket_pct >= 95 {
(
StampStatus::Critical,
Some(if b.immutable {
"immutable batch will REJECT next upload at this bucket.".into()
} else {
"mutable batch will silently overwrite oldest chunks.".into()
}),
)
} else if worst_bucket_pct >= 80 {
(
StampStatus::Skewed,
Some(format!(
"worst bucket {worst_bucket_pct}% > safe headroom — dilute or stop using."
)),
)
} else {
(StampStatus::Healthy, None)
};
StampRow {
label,
batch_id_short,
volume,
worst_bucket_pct,
worst_bucket_raw,
ttl,
immutable: b.immutable,
status,
why,
}
}
fn worst_bucket_pct(b: &PostageBatch) -> u32 {
let upper_bound: u32 = 1u32 << b.depth.saturating_sub(b.bucket_depth);
if upper_bound == 0 {
0
} else {
let pct = (u64::from(b.utilization) * 100) / u64::from(upper_bound);
pct.min(100) as u32
}
}
fn format_bytes(bytes: u128) -> String {
const K: u128 = 1024;
const M: u128 = K * 1024;
const G: u128 = M * 1024;
const T: u128 = G * 1024;
if bytes >= T {
format!("{:.1} TiB", bytes as f64 / T as f64)
} else if bytes >= G {
format!("{:.1} GiB", bytes as f64 / G as f64)
} else if bytes >= M {
format!("{:.1} MiB", bytes as f64 / M as f64)
} else if bytes >= K {
format!("{:.1} KiB", bytes as f64 / K as f64)
} else {
format!("{bytes} B")
}
}
fn format_ttl_seconds(secs: i64) -> String {
if secs <= 0 {
return "expired".into();
}
let days = secs / 86_400;
let hours = (secs % 86_400) / 3_600;
if days >= 1 {
format!("{days}d {hours:>2}h")
} else {
let minutes = (secs % 3_600) / 60;
format!("{hours}h {minutes:>2}m")
}
}
fn fill_bar(pct: u32, width: usize) -> String {
let filled = ((pct as usize) * width) / 100;
let mut bar = String::with_capacity(width);
for _ in 0..filled.min(width) {
bar.push('▇');
}
for _ in filled.min(width)..width {
bar.push('░');
}
bar
}
impl Component for Stamps {
fn update(&mut self, action: Action) -> Result<Option<Action>> {
if matches!(action, Action::Tick) {
self.pull_latest();
self.drain_fetches();
}
Ok(None)
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
if matches!(
self.drill,
DrillState::Loaded { .. } | DrillState::Loading { .. } | DrillState::Failed { .. }
) && matches!(key.code, KeyCode::Esc)
{
self.drill = DrillState::Idle;
return Ok(None);
}
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
let n = self.snapshot.batches.len();
if n > 0 && self.selected + 1 < n {
self.selected += 1;
}
}
KeyCode::Char('k') | KeyCode::Up => {
self.selected = self.selected.saturating_sub(1);
}
KeyCode::Enter => {
self.maybe_start_drill();
}
_ => {}
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let chunks = Layout::vertical([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(1), ])
.split(area);
let count = self.snapshot.batches.len();
let mut header_l1 = vec![
Span::styled("STAMPS", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(format!(" {count} batch(es)")),
];
if let DrillState::Loaded { batch_id, .. }
| DrillState::Loading { batch_id }
| DrillState::Failed { batch_id, .. } = &self.drill
{
let hex = batch_id.to_hex();
let short = if hex.len() > 8 { &hex[..8] } else { &hex };
header_l1.push(Span::raw(format!(" · drill {short}…")));
}
let header_l1 = Line::from(header_l1);
let mut header_l2 = Vec::new();
let t = theme::active();
if let Some(err) = &self.snapshot.last_error {
let (color, msg) = theme::classify_header_error(err);
header_l2.push(Span::styled(msg, Style::default().fg(color)));
} else if !self.snapshot.is_loaded() {
header_l2.push(Span::styled(
format!("{} loading…", theme::spinner_glyph()),
Style::default().fg(t.dim),
));
}
frame.render_widget(
Paragraph::new(vec![header_l1, Line::from(header_l2)])
.block(Block::default().borders(Borders::BOTTOM)),
chunks[0],
);
match &self.drill {
DrillState::Idle => self.draw_table(frame, chunks[1]),
DrillState::Loading { .. } => {
let msg = Line::from(Span::styled(
" fetching /stamps/<id>/buckets… (Esc cancel)",
Style::default().fg(t.dim),
));
frame.render_widget(Paragraph::new(msg), chunks[1]);
}
DrillState::Failed { error, .. } => {
let msg = Line::from(vec![
Span::raw(" drill failed: "),
Span::styled(error.clone(), Style::default().fg(t.fail)),
Span::raw(" (Esc to dismiss)"),
]);
frame.render_widget(Paragraph::new(msg), chunks[1]);
}
DrillState::Loaded { view, .. } => self.draw_drill(frame, chunks[1], view),
}
let footer = match &self.drill {
DrillState::Idle => Line::from(vec![
Span::styled(" Tab ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" switch screen "),
Span::styled(
" ↑↓/jk ",
Style::default().fg(Color::Black).bg(Color::White),
),
Span::raw(" select "),
Span::styled(" ↵ ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" drill "),
Span::styled(" ? ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" help "),
Span::styled(" q ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" quit "),
Span::styled(" I/M ", Style::default().fg(t.dim)),
Span::raw(" immutable / mutable "),
]),
_ => Line::from(vec![
Span::styled(" Esc ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" close drill "),
Span::styled(" Tab ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" switch screen "),
Span::styled(" ? ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" help "),
Span::styled(" q ", Style::default().fg(Color::Black).bg(Color::White)),
Span::raw(" quit "),
]),
};
frame.render_widget(Paragraph::new(footer), chunks[2]);
Ok(())
}
}
impl Stamps {
fn draw_table(&mut self, frame: &mut Frame, area: Rect) {
use ratatui::layout::{Constraint, Layout};
let t = theme::active();
let table_chunks =
Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).split(area);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
" LABEL BATCH VOLUME WORST BUCKET TTL STATUS",
Style::default()
.fg(t.dim)
.add_modifier(Modifier::BOLD),
))),
table_chunks[0],
);
if self.snapshot.batches.is_empty() {
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
" (no batches yet — buy one with swarm-cli or `bee stamps buy`)",
Style::default().fg(t.dim).add_modifier(Modifier::ITALIC),
))),
table_chunks[1],
);
return;
}
let mut lines: Vec<Line> = Vec::new();
let mut row_starts: Vec<usize> = Vec::new();
for (i, r) in Self::rows_for(&self.snapshot).into_iter().enumerate() {
row_starts.push(lines.len());
let bar = fill_bar(r.worst_bucket_pct, 8);
let immut_glyph = if r.immutable { "I" } else { "M" };
let cursor = if i == self.selected {
format!("{} ", t.glyphs.cursor)
} else {
" ".to_string()
};
lines.push(Line::from(vec![
Span::styled(
cursor,
Style::default()
.fg(if i == self.selected { t.accent } else { t.dim })
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!("{:<20}", truncate(&r.label, 20)),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{:<13}", r.batch_id_short)),
Span::raw(format!("{:<12}", r.volume)),
Span::styled(
format!("{bar} {:>3}% ({})", r.worst_bucket_pct, r.worst_bucket_raw),
Style::default().fg(bucket_color(r.worst_bucket_pct)),
),
Span::raw(" "),
Span::raw(format!("{:<10} ", r.ttl)),
Span::styled(immut_glyph, Style::default().fg(t.dim)),
Span::raw(" "),
Span::styled(
r.status.label(),
Style::default()
.fg(r.status.color())
.add_modifier(Modifier::BOLD),
),
]));
if let Some(why) = r.why {
lines.push(Line::from(vec![
Span::raw(format!(" {} ", t.glyphs.continuation)),
Span::styled(
why,
Style::default().fg(t.dim).add_modifier(Modifier::ITALIC),
),
]));
}
}
let visual_cursor = row_starts.get(self.selected).copied().unwrap_or(0);
let body = table_chunks[1];
let visible_rows = body.height as usize;
self.scroll_offset = super::scroll::clamp_scroll(
visual_cursor,
self.scroll_offset,
visible_rows,
lines.len(),
);
frame.render_widget(
Paragraph::new(lines.clone()).scroll((self.scroll_offset as u16, 0)),
body,
);
super::scroll::render_scrollbar(frame, body, self.scroll_offset, visible_rows, lines.len());
}
fn draw_drill(&self, frame: &mut Frame, area: Rect, view: &StampDrillView) {
let t = theme::active();
let mut lines: Vec<Line> = Vec::new();
let total_buckets: u32 = view.fill_distribution.iter().sum();
lines.push(Line::from(vec![
Span::raw(" depth "),
Span::styled(
format!("{}", view.depth),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(" bucket-depth "),
Span::styled(
format!("{}", view.bucket_depth),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(" per-bucket cap "),
Span::styled(
format!("{}", view.upper_bound),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(
format!("{} buckets", total_buckets),
Style::default().fg(t.dim),
),
]));
lines.push(Line::from(vec![
Span::raw(" total chunks "),
Span::styled(
format!("{}", view.total_chunks),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(" / "),
Span::styled(
format!("{}", view.theoretical_capacity),
Style::default().fg(t.dim),
),
Span::raw(" worst bucket "),
Span::styled(
format!("{}%", view.worst_pct),
Style::default()
.fg(bucket_color(view.worst_pct))
.add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
" FILL % COUNT DISTRIBUTION",
Style::default().fg(t.dim).add_modifier(Modifier::BOLD),
)));
let max_bin = view
.fill_distribution
.iter()
.copied()
.max()
.unwrap_or(1)
.max(1);
for (idx, count) in view.fill_distribution.iter().enumerate() {
let label = FILL_BIN_LABELS[idx];
let bar_width = ((u64::from(*count) * 30) / u64::from(max_bin)) as usize;
let bar: String = std::iter::repeat_n('▇', bar_width).collect();
let bin_color = match idx {
5 => t.fail,
4 => t.warn,
_ => t.pass,
};
lines.push(Line::from(vec![
Span::raw(" "),
Span::raw(format!("{label:<10} ")),
Span::styled(
format!("{count:>5} "),
Style::default().add_modifier(Modifier::BOLD),
),
Span::styled(bar, Style::default().fg(bin_color)),
]));
}
lines.push(Line::from(""));
if !view.worst_buckets.is_empty() {
lines.push(Line::from(Span::styled(
" WORST BUCKETS",
Style::default().fg(t.dim).add_modifier(Modifier::BOLD),
)));
for w in &view.worst_buckets {
if w.collisions == 0 {
break;
}
lines.push(Line::from(vec![
Span::raw(" "),
Span::raw(format!("#{:<8}", w.bucket_id)),
Span::raw(format!("{:>4} / {} ", w.collisions, view.upper_bound)),
Span::styled(
format!("{}%", w.pct),
Style::default()
.fg(bucket_color(w.pct))
.add_modifier(Modifier::BOLD),
),
]));
}
}
frame.render_widget(Paragraph::new(lines), area);
}
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
out.push('…');
out
}
}
fn bucket_color(pct: u32) -> Color {
let t = theme::active();
if pct >= 95 {
t.fail
} else if pct >= 80 {
t.warn
} else {
t.pass
}
}
#[cfg(test)]
mod tests {
use super::*;
fn buckets_with(counts: &[(u32, u32)], depth: u8, bucket_depth: u8) -> PostageBatchBuckets {
let upper_bound = 1u32 << (depth - bucket_depth);
let buckets = counts
.iter()
.map(|(id, c)| BatchBucket {
bucket_id: *id,
collisions: *c,
})
.collect();
PostageBatchBuckets {
depth,
bucket_depth,
bucket_upper_bound: upper_bound,
buckets,
}
}
#[test]
fn fill_bar_clamps_to_width() {
assert_eq!(fill_bar(0, 8), "░░░░░░░░");
assert_eq!(fill_bar(50, 8), "▇▇▇▇░░░░");
assert_eq!(fill_bar(100, 8), "▇▇▇▇▇▇▇▇");
assert_eq!(fill_bar(150, 8), "▇▇▇▇▇▇▇▇"); }
#[test]
fn format_bytes_iec() {
assert_eq!(format_bytes(0), "0 B");
assert_eq!(format_bytes(1024), "1.0 KiB");
assert_eq!(format_bytes(1024 * 1024), "1.0 MiB");
assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GiB");
assert_eq!(format_bytes(16 * 1024 * 1024 * 1024), "16.0 GiB");
}
#[test]
fn format_ttl_zero_is_expired() {
assert_eq!(format_ttl_seconds(0), "expired");
assert_eq!(format_ttl_seconds(-5), "expired");
}
#[test]
fn format_ttl_days_and_hours() {
assert_eq!(format_ttl_seconds(47 * 86_400 + 12 * 3_600), "47d 12h");
}
#[test]
fn format_ttl_under_a_day_uses_hours_minutes() {
assert_eq!(format_ttl_seconds(2 * 3_600 + 30 * 60), "2h 30m");
}
#[test]
fn drill_view_bins_and_worst_n() {
let buckets = buckets_with(
&[
(0, 64), (1, 60), (2, 40), (3, 20), (4, 1), (5, 0), ],
22,
16,
);
let view = Stamps::compute_drill_view(&buckets);
assert_eq!(view.depth, 22);
assert_eq!(view.bucket_depth, 16);
assert_eq!(view.upper_bound, 64);
assert_eq!(view.total_chunks, 64 + 60 + 40 + 20 + 1);
assert_eq!(view.fill_distribution, [1, 1, 1, 1, 1, 1]);
assert_eq!(view.worst_pct, 100);
assert_eq!(view.worst_buckets.len(), 6);
assert_eq!(view.worst_buckets[0].bucket_id, 0);
assert_eq!(view.worst_buckets[0].pct, 100);
assert_eq!(view.worst_buckets[1].bucket_id, 1);
assert_eq!(view.worst_buckets[1].pct, 93);
}
#[test]
fn drill_view_handles_empty_buckets() {
let buckets = buckets_with(&[], 22, 16);
let view = Stamps::compute_drill_view(&buckets);
assert_eq!(view.total_chunks, 0);
assert_eq!(view.fill_distribution, [0; 6]);
assert_eq!(view.worst_pct, 0);
assert!(view.worst_buckets.is_empty());
}
#[test]
fn drill_view_caps_worst_at_ten() {
let entries: Vec<(u32, u32)> = (0..12).map(|i| (i, 1)).collect();
let buckets = buckets_with(&entries, 22, 16);
let view = Stamps::compute_drill_view(&buckets);
assert_eq!(view.worst_buckets.len(), 10);
}
#[test]
fn drill_view_breaks_ties_by_bucket_id() {
let buckets = buckets_with(&[(7, 5), (3, 5), (10, 5)], 22, 16);
let view = Stamps::compute_drill_view(&buckets);
assert_eq!(
view.worst_buckets
.iter()
.map(|w| w.bucket_id)
.collect::<Vec<_>>(),
vec![3, 7, 10],
);
}
#[test]
fn fill_bin_handles_overflow_collisions() {
assert_eq!(bucket_fill_bin(70, 64), 5);
}
}