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
34 changes: 34 additions & 0 deletions .github/workflows/maintenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Maintenance

on:
schedule:
- cron: "41 7 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
credentials:
runs-on: ubuntu-latest
steps:
- name: Check release credentials
env:
TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
headers=$(curl -sS -o /dev/null -D - -w '%{http_code}' \
-H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/BlockIo/homebrew-tap)
status=$(printf '%s' "$headers" | tail -n 1)
if [ "$status" != "200" ]; then
echo "::error::HOMEBREW_TAP_TOKEN was rejected (HTTP $status); replace it"
exit 1
fi
expires=$(printf '%s' "$headers" | tr -d '\r' \
| sed -n 's/^github-authentication-token-expiration: //Ip')
[ -z "$expires" ] && exit 0
days=$(( ($(date -d "$expires" +%s) - $(date +%s)) / 86400 ))
if [ "$days" -lt 30 ]; then
echo "::error::HOMEBREW_TAP_TOKEN expires in $days days; replace it"
exit 1
fi
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ names, environment variables, exit codes, error codes, and the shape of

## [Unreleased]

## [0.6.0] - 2026-09-29

### Added

- Dollar values at the time, back to December 2013: `tx` shows what the
amount sent and the fee were worth, `block` the DOGE price and what the
reward was worth, and `address` each history entry's value. `--json` is
unchanged.

## [0.5.2] - 2026-09-29

### Changed
Expand Down Expand Up @@ -123,7 +132,8 @@ names, environment variables, exit codes, error codes, and the shape of
- Installers: Homebrew (`blockio/tap/dogechain`), npm (`dogechain`), shell script, PowerShell,
and prebuilt binaries for macOS, Linux and Windows.

[Unreleased]: https://github.com/BlockIo/dogechain-cli/compare/v0.5.2...HEAD
[Unreleased]: https://github.com/BlockIo/dogechain-cli/compare/v0.6.0...HEAD
[0.6.0]: https://github.com/BlockIo/dogechain-cli/compare/v0.5.2...v0.6.0
[0.5.2]: https://github.com/BlockIo/dogechain-cli/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/BlockIo/dogechain-cli/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/BlockIo/dogechain-cli/compare/v0.4.0...v0.5.0
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dogechain-cli"
version = "0.5.2"
version = "0.6.0"
edition = "2024"
rust-version = "1.88"
description = "Command-line interface for Dogechain.com"
Expand Down
71 changes: 69 additions & 2 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ fn show_block(out: &mut dyn Write, b: &Block) -> Result<()> {
b.fees,
b.value_out.display(2)
)?;
if let Some(p) = b.price.as_ref().and_then(Price::usd) {
writeln!(
out,
"Price then {} per DOGE · reward worth about {}",
format_usd_price(p),
format_usd(doge_to_f64(b.reward) * p)
)?;
}
writeln!(
out,
"Confirmations {}",
Expand Down Expand Up @@ -260,6 +268,17 @@ fn show_tx(out: &mut dyn Write, r: &TxReply, explain_only: bool) -> Result<()> {
}
writeln!(out, "{}", r.explain.headline)?;
writeln!(out, "{}", r.explain.detail)?;
let usd = r.transaction.price.as_ref().and_then(Price::usd);
if let (Some(sent), Some(p)) = (r.explain.sent, usd)
&& sent.0 > 0
{
writeln!(
out,
"Worth about {} at the time (1 DOGE = {})",
format_usd(doge_to_f64(sent) * p),
format_usd_price(p)
)?;
}
if explain_only {
return Ok(());
}
Expand Down Expand Up @@ -293,8 +312,11 @@ fn show_tx(out: &mut dyn Write, r: &TxReply, explain_only: bool) -> Result<()> {
}
writeln!(
out,
"Fee {} DOGE · {} bytes",
"Fee {} DOGE{} · {} bytes",
tx.fee,
usd.filter(|_| tx.fee.0 > 0)
.map(|p| format!(" ({})", format_usd(doge_to_f64(tx.fee) * p)))
.unwrap_or_default(),
group_thousands(tx.size.into())
)?;
writeln!(out, "Transaction id {}", tx.hash)?;
Expand Down Expand Up @@ -362,9 +384,15 @@ fn show_address(out: &mut dyn Write, r: &AddressReply) -> Result<()> {
} else {
""
};
let worth = h
.price
.as_ref()
.and_then(Price::usd)
.map(|p| format!(" ({})", format_usd(doge_to_f64(h.balance_change.abs()) * p)))
.unwrap_or_default();
writeln!(
out,
" {sign}{} DOGE {when} {}{p2pk}",
" {sign}{} DOGE{worth} {when} {}{p2pk}",
h.balance_change.abs(),
h.hash
)?;
Expand Down Expand Up @@ -764,6 +792,33 @@ fn plural(n: u64, noun: &str) -> String {
)
}

fn doge_to_f64(d: Doge) -> f64 {
d.0 as f64 / KOINU_PER_DOGE
}

/// A dollar amount: `$0.52`, `$1,234.56`, `$33,100`; `<$0.01` for dust.
fn format_usd(v: f64) -> String {
if v > 0.0 && v < 0.01 {
"<$0.01".into()
} else if v < 10_000.0 {
let cents = (v * 100.0).round() as i128;
format!("${}.{:02}", group_thousands(cents / 100), cents % 100)
} else {
format!("${}", group_thousands(v.round() as i128))
}
}

/// The price of 1 DOGE with enough digits to be meaningful: `$0.0939`, and
/// for early prices four significant digits, e.g. `$0.001565`.
fn format_usd_price(p: f64) -> String {
if p >= 0.01 || p <= 0.0 {
return format!("${p:.4}");
}
let decimals = (4 - p.log10().floor() as i32 - 1).clamp(4, 10) as usize;
let s = format!("{p:.decimals$}");
format!("${}", s.trim_end_matches('0'))
}

/// Koinu per byte to DOGE per kB (1,000 bytes), the unit wallets show.
fn doge_per_kb(koinu_per_byte: f64) -> String {
let doge = koinu_per_byte * 1000.0 / KOINU_PER_DOGE;
Expand All @@ -785,6 +840,18 @@ fn format_number(v: f64) -> String {
mod tests {
use super::*;

#[test]
fn formats_dollars() {
assert_eq!(format_usd(33_100.4), "$33,100");
assert_eq!(format_usd(1234.567), "$1,234.57");
assert_eq!(format_usd(0.52), "$0.52");
assert_eq!(format_usd(0.001), "<$0.01");
assert_eq!(format_usd(0.0), "$0.00");
assert_eq!(format_usd_price(0.0938771), "$0.0939");
assert_eq!(format_usd_price(0.00156501), "$0.001565");
assert_eq!(format_usd_price(0.000183), "$0.000183");
}

#[test]
fn formats_hashrate_and_difficulty() {
assert_eq!(format_hashrate("2789460872749466"), "2.79 PH/s");
Expand Down
31 changes: 31 additions & 0 deletions src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ use serde::Deserialize;

use crate::amount::Doge;

/// The DOGE price when a block was mined (or, for unconfirmed transactions,
/// now). `value` is the price of 1 DOGE, as a decimal string.
#[derive(Debug, Clone, Deserialize)]
pub struct Price {
pub value: String,
pub currency: String,
}

impl Price {
/// USD per DOGE, if this is a USD price.
pub fn usd(&self) -> Option<f64> {
if self.currency != "USD" {
return None;
}
self.value
.trim()
.parse()
.ok()
.filter(|p: &f64| p.is_finite() && *p >= 0.0)
}
}

#[derive(Debug, Deserialize)]
pub struct Block {
pub height: u64,
Expand All @@ -29,6 +51,8 @@ pub struct Block {
pub difficulty: String,
pub previous_block_hash: Option<String>,
pub next_block_hash: Option<String>,
#[serde(default)]
pub price: Option<Price>,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -63,6 +87,8 @@ pub struct Transaction {
pub size: u64,
pub inputs: Vec<Input>,
pub outputs: Vec<Output>,
#[serde(default)]
pub price: Option<Price>,
}

#[derive(Debug, Deserialize)]
Expand All @@ -83,6 +109,9 @@ pub struct Output {
pub struct Explain {
pub headline: String,
pub detail: String,
/// What the sender paid out, excluding change and fee.
#[serde(default)]
pub sent: Option<Doge>,
#[serde(default)]
pub change_outputs: Vec<u32>,
}
Expand Down Expand Up @@ -119,6 +148,8 @@ pub struct AddressTx {
pub block: Option<u64>,
pub time: i64,
pub balance_change: Doge,
#[serde(default)]
pub price: Option<Price>,
/// The coins were paid to the address's public key (P2PK), not to the
/// address itself. The API sends it only when true.
#[serde(default)]
Expand Down
24 changes: 24 additions & 0 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,3 +1004,27 @@ fn mcp_print_changes_nothing() {
"codex mcp add dogechain --url https://dogechain.com/mcp"
);
}

#[test]
fn shows_dollar_values_at_the_time() {
let mut priced = tx(json!([]));
priced["transaction"]["price"] = json!({ "currency": "USD", "value": "0.00156501" });
dogechain(&mock(vec![ok(priced)]))
.args(["tx", TXID])
.assert()
.success()
.stdout(predicate::str::contains(
"Worth about $0.09 at the time (1 DOGE = $0.001565)",
))
.stdout(predicate::str::contains("Fee 0.01 DOGE (<$0.01)"));

let mut b = block();
b["price"] = json!({ "currency": "USD", "value": "0.09387710" });
dogechain(&mock(vec![ok(b)]))
.args(["block", "latest"])
.assert()
.success()
.stdout(predicate::str::contains(
"Price then $0.0939 per DOGE · reward worth about $938.77",
));
}
Loading