Skip to content

Commit

Permalink
electrum: New welcome banner (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
shesek committed Sep 30, 2020
1 parent 13c4cc9 commit 223a242
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 42 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
- Electrum plugin: Compatibility with Electrum v4 — *except for lightning* which is
[tricky with personal servers](https://github.com/chris-belcher/electrum-personal-server/issues/174#issuecomment-577619460) (#53)

- Electrum: New welcome banner (#44)

- Scriptable transaction broadcast command via `--tx-broadcast-cmd <cmd>` (#7)

The command will be used in place of broadcasting transactions using the full node,
Expand Down
4 changes: 2 additions & 2 deletions scripts/setup-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ if [ -z "$NO_FUNDING" ]; then
fi

echo Setting up bwt
runbwt --network regtest --bitcoind-dir $BTC_DIR --bitcoind-url http://localhost:$BTC_RPC_PORT/ \
--bitcoind-wallet bwt \
runbwt --no-startup-banner --network regtest \
--bitcoind-dir $BTC_DIR --bitcoind-url http://localhost:$BTC_RPC_PORT/ --bitcoind-wallet bwt \
--electrum-rpc-addr $BWT_ELECTRUM_ADDR \
--unix-listener-path $BWT_SOCKET --poll-interval ${INTERVAL:=120} \
--initial-import-size 30 \
Expand Down
9 changes: 6 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use bitcoincore_rpc::{self as rpc, Client as RpcClient, RpcApi};

use crate::bitcoincore_ext::RpcApiExt;
use crate::util::debounce_sender;
use crate::{Config, HDWallet, HDWatcher, Indexer, Query, Result};
use crate::{banner, Config, HDWallet, HDWatcher, Indexer, Query, Result};

#[cfg(feature = "electrum")]
use crate::electrum::ElectrumServer;
Expand Down Expand Up @@ -58,6 +58,10 @@ impl App {

wait_bitcoind(&rpc)?;

if config.startup_banner {
println!("{}", banner::get_welcome_banner(&query)?);
}

// do an initial sync without keeping track of updates
indexer.write().unwrap().initial_sync()?;

Expand Down Expand Up @@ -153,12 +157,11 @@ fn wait_bitcoind(rpc: &RpcClient) -> Result<()> {
let netinfo = rpc.get_network_info_()?;
let mut bcinfo = rpc.get_blockchain_info()?;
info!(
"bwt v{} connected to {} on {}, protocolversion={}, pruned={}, bestblock={}",
"bwt v{} connected to {} on {}, protocolversion={}, bestblock={}",
crate::BWT_VERSION,
netinfo.subversion,
bcinfo.chain,
netinfo.protocol_version,
bcinfo.pruned,
bcinfo.best_block_hash
);

Expand Down
290 changes: 290 additions & 0 deletions src/banner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
use std::time::{Duration as StdDuration, UNIX_EPOCH};

use chrono::Duration;

use bitcoin::{blockdata::constants, Amount};
use bitcoincore_rpc::RpcApi;

use crate::bitcoincore_ext::RpcApiExt;
use crate::{Query, Result};

const DIFFCHANGE_INTERVAL: u64 = constants::DIFFCHANGE_INTERVAL as u64;
const TARGET_BLOCK_SPACING: u64 = constants::TARGET_BLOCK_SPACING as u64;
const INITIAL_REWARD: u64 = 50 * constants::COIN_VALUE;
const HALVING_INTERVAL: u64 = 210_000;
const MAX_BLOCK_WEIGHT: f64 = constants::MAX_BLOCK_WEIGHT as f64;

pub fn get_welcome_banner(query: &Query) -> Result<String> {
let rpc = query.rpc();

let net_info = rpc.get_network_info_()?;
let chain_info = rpc.get_blockchain_info()?;
let mempool_info = rpc.get_mempool_info()?;
let net_totals = rpc.get_net_totals()?;
let peers = rpc.get_peer_info()?;
let hash_rate_7d = rpc.get_network_hash_ps(1008)?;
let uptime = dur_from_secs(rpc.uptime()?);
let tip = rpc.get_block_stats(&rpc.get_best_block_hash()?)?;

let est_fee = |target| {
query
.estimate_fee(target)
.ok()
.flatten()
.map_or("ₙ.ₐ.".into(), |rate| format!("{:.1}", rate))
};
let est_20m = est_fee(2u16);
let est_3h = est_fee(18u16);
let est_1d = est_fee(144u16);

let mut chain_name = chain_info.chain;
if chain_name == "main" || chain_name == "test" {
chain_name = format!("{}net", chain_name)
};

// 24 hour average bandwidth usage
let num_days = uptime.num_seconds() as f64 / 86400f64;
let bandwidth_up = (net_totals.total_bytes_sent as f64 / num_days) as u64;
let bandwidth_down = (net_totals.total_bytes_recv as f64 / num_days) as u64;

// Time until the next difficulty adjustment
let retarget_blocks = DIFFCHANGE_INTERVAL - (tip.height % DIFFCHANGE_INTERVAL);
let retarget_dur = dur_from_secs(retarget_blocks * TARGET_BLOCK_SPACING);

// Current reward era and time until next halving
let reward_era = tip.height / HALVING_INTERVAL;
let block_reward = Amount::from_sat(INITIAL_REWARD / 2u64.pow(reward_era as u32));
let halving_blocks = HALVING_INTERVAL - (tip.height % HALVING_INTERVAL);
let halving_dur = dur_from_secs(halving_blocks * TARGET_BLOCK_SPACING);

// Time since last block
let tip_ago = match (UNIX_EPOCH + StdDuration::from_secs(tip.time as u64)).elapsed() {
Ok(elapsed) => format!("{} ago", format_dur(&Duration::from_std(elapsed).unwrap())),
Err(_) => "just now".to_string(), // account for blocks with a timestamp slightly in the future
};

// let tip_full = tip.total_weight as f64 / MAX_BLOCK_WEIGHT * 100f64;
// / {tip_full:.1}% Fᴜʟʟ

// sat/kb -> sat/vB
let mempool_min_fee = mempool_info.mempool_min_fee.as_sat() as f64 / 1000f64;

let has_inbound = peers.iter().any(|p| p.inbound);

let modes = [
if chain_info.pruned {
"✂️ ᴘʀᴜɴᴇᴅ"
} else {
"🗄️ ᴀʀᴄʜɪᴠᴀʟ"
},
if net_info.local_relay {
"🗣️ ᴍᴇᴍᴘᴏᴏʟ ʀᴇʟᴀʏ"
} else {
"📦 ʙʟᴏᴄᴋsᴏɴʟʏ"
},
if has_inbound {
"👂 ʟɪsᴛᴇɴs"
} else {
"🙉 ɴᴏʟɪsᴛᴇɴ"
},
];

let ver_lines = big_numbers(crate::BWT_VERSION);

Ok(format!(
r#"
██████  ██  ██ ████████ 
██   ██ ██  ██    ██    
██████  ██  █  ██  ██  {ver_line1}
██   ██ ██ ███ ██  ██  █ █ {ver_line2}
██████   ███ ███   ██  ▀▄▀ {ver_line3}
{client_name}
{modes}
NETWORK: 🌐 {chain_name}
CONNECTED: 💻 {connected_peers} ᴘᴇᴇʀs
UPTIME: ⏱️ {uptime}
BANDWIDTH: 📶 {bandwidth_up} 🔼 {bandwidth_down} 🔽 (24ʜ ᴀᴠɢ)
CHAIN SIZE: 💾 {chain_size}
HASHRATE: ⛏️ {hash_rate} (7ᴅ ᴀᴠɢ)
DIFFICULTY: 🏋️ {difficulty} (ʀᴇ-🎯 ɪɴ {retarget_dur} ⏳)
REWARD ERA: 🎁 {block_reward:.2} ʙᴛᴄ (½ ɪɴ {halving_dur} ⏳)
LAST BLOCK: ⛓️ {tip_height} / {tip_ago} / {tip_size} / {tip_n_tx} / {tip_full:.1}% Fᴜʟʟ
Fᴇᴇ ʀᴀᴛᴇ {tip_fee_per10}-{tip_fee_per90} sᴀᴛ/ᴠʙ / ᴀᴠɢ {tip_fee_avg} sᴀᴛ/ᴠʙ / ᴛᴏᴛᴀʟ {tip_fee_total:.3} ʙᴛᴄ
MEMPOOL: 💭 {mempool_size} / {mempool_n_tx} / ᴍɪɴ {mempool_min_fee} sᴀᴛ/ᴠʙ
FEES EST: 🏷️ 20 ᴍɪɴᴜᴛᴇs: {est_20m} / 3 ʜᴏᴜʀs: {est_3h} / 1 ᴅᴀʏ: {est_1d} (sᴀᴛ/ᴠʙ)
SUPPORT DEV: 🚀 bc1qmuagsjvq0lh3admnafk0qnlql0vvxv08au9l2d / https://btcpay.shesek.info
"#,
client_name = to_widetext(&net_info.subversion),
chain_name = to_smallcaps(&chain_name),
connected_peers = net_info.connections,
uptime = to_smallcaps(&format_dur(&uptime).to_uppercase()),
bandwidth_up = to_smallcaps(&format_bytes(bandwidth_up)),
bandwidth_down = to_smallcaps(&format_bytes(bandwidth_down)),
chain_size = to_smallcaps(&format_bytes(chain_info.size_on_disk)),
hash_rate = to_smallcaps(&format_metric(hash_rate_7d, " ", "H/s")),
difficulty = to_smallcaps(&format_metric(chain_info.difficulty as f64, " ", "")),
retarget_dur = to_smallcaps(&format_dur(&retarget_dur).to_uppercase()),
halving_dur = to_smallcaps(&format_dur(&halving_dur).to_uppercase()),
block_reward = block_reward.as_btc(),
tip_height = tip.height,
tip_ago = to_smallcaps(&tip_ago),
tip_size = to_smallcaps(&format_bytes(tip.total_size as u64)),
tip_n_tx = to_smallcaps(&format_metric(tip.txs as f64, "", " txs")),
tip_fee_per10 = tip.feerate_percentiles.0,
tip_fee_per90 = tip.feerate_percentiles.4,
tip_fee_avg = tip.avg_fee_rate,
tip_fee_total = tip.total_fee.as_btc(),
mempool_size = to_smallcaps(&format_bytes(mempool_info.bytes)),
mempool_n_tx = to_smallcaps(&format_metric(mempool_info.size as f64, "", " txs")),
mempool_min_fee = mempool_min_fee,
est_20m = est_20m,
est_3h = est_3h,
est_1d = est_1d,
modes = modes.join(" "),
ver_line1 = ver_lines.0,
ver_line2 = ver_lines.1,
ver_line3 = ver_lines.2,
))
}

/* Disabled because this takes too long
let utxo_info = query.rpc().get_tx_out_set_info()?;
let total_supply = utxo_info.total_amount.to_string();
𝚄𝚃𝚇𝙾 𝚂𝙸𝚉𝙴: 🗃️ {utxo_size}
✔️ 𝚅𝙴𝚁𝙸𝙵𝙸𝙴𝙳 ✔️
𝙲𝙸𝚁𝙲𝚄𝙻𝙰𝚃𝙸𝙽𝙶 {total_supply}
𝚂𝚄𝙿𝙿𝙻𝚈 {total_supply_line}
utxo_size = to_smallcaps(&format_bytes(utxo_info.disk_size)),
total_supply = to_smallcaps(&total_supply),
total_supply_line = "‾".repeat(total_supply.len()),
height = utxo_info.height,
*/

fn dur_from_secs(seconds: u64) -> Duration {
Duration::from_std(StdDuration::from_secs(seconds)).unwrap()
}

fn format_dur(dur: &Duration) -> String {
let days = dur.num_days();
if days > 90 {
return format!("{} months", days / 30);
}
if days > 21 {
return format!("{} weeks", days / 7);
}
if days > 3 {
return format!("{} days", days);
}
let hours = dur.num_hours();
if hours > 3 {
return format!("{} hours", hours);
}
let minutes = dur.num_minutes();
if minutes > 3 {
return format!("{} minutes", minutes);
}
format!("{} seconds", dur.num_seconds())
}

fn format_bytes(bytes: u64) -> String {
format_metric(bytes as f64, " ", "B")
}

fn format_metric(num: f64, space: &str, suf: &str) -> String {
if num >= 1000000000000000000f64 {
format!(
"{}{}E{}",
format_dec(num / 1000000000000000000f64),
space,
suf
)
} else if num >= 1000000000000000f64 {
format!("{}{}P{}", format_dec(num / 1000000000000000f64), space, suf)
} else if num >= 1000000000000f64 {
format!("{}{}T{}", format_dec(num / 1000000000000f64), space, suf)
} else if num >= 1000000000f64 {
format!("{}{}G{}", format_dec(num / 1000000000f64), space, suf)
} else if num >= 1000000f64 {
format!("{}{}M{}", format_dec(num / 1000000f64), space, suf)
} else if num >= 1000f64 {
format!("{}{}K{}", format_dec(num / 1000f64), space, suf)
} else {
format!("{}{}{}", format_dec(num), space, suf)
}
}

// format with 1 decimal digit and no unnecessary trailing 0s or dots
fn format_dec(num: f64) -> String {
format!("{:.1}", num)
.trim_end_matches('0')
.trim_end_matches('.')
.into()
}

lazy_static! {
static ref SMALLCAPS_ALPHABET: Vec<char> =
"ᴀʙᴄᴅᴇFɢʜɪᴊᴋʟᴍɴᴏᴘQʀsᴛᴜᴠᴡxʏᴢᴀʙᴄᴅᴇFɢʜɪᴊᴋʟᴍɴᴏᴘQʀsᴛᴜᴠᴡxʏᴢ01234567890./:".chars().collect::<Vec<_>>();
static ref WIDETEXT_ALPHABET: Vec<char> =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890./:"
.chars()
.collect::<Vec<_>>();
}

fn convert_alphabet(s: &str, alphabet: &[char]) -> String {
s.chars()
.map(|c| match c {
'a'..='z' => alphabet[c as usize - 97],
'A'..='Z' => alphabet[c as usize - 65 + 26],
'0'..='9' => alphabet[c as usize - 48 + 26 * 2],
'.' => alphabet[63],
'/' => alphabet[64],
':' => alphabet[65],
c => c,
})
.collect()
}

fn to_smallcaps(s: &str) -> String {
convert_alphabet(s, &SMALLCAPS_ALPHABET[..])
}
fn to_widetext(s: &str) -> String {
convert_alphabet(s, &WIDETEXT_ALPHABET[..])
}

fn big_numbers(s: &str) -> (String, String, String) {
let mut lines = ("".to_string(), "".to_string(), "".to_string());
for c in s.chars() {
let char_lines = match c {
'0' => ("█▀▀█", "█ █", "█▄▄█"),
'1' => ("▄█ ", " █ ", "▄█▄"),
'2' => ("█▀█", " ▄▀", "█▄▄"),
'3' => ("█▀▀█", " ▀▄", "█▄▄█"),
'4' => (" █▀█ ", "█▄▄█▄", " █ "),
'5' => ("█▀▀", "▀▀▄", "▄▄▀"),
'6' => ("▄▀▀▄", "█▄▄ ", "▀▄▄▀"),
'7' => ("▀▀▀█", " █ ", " ▐▌ "),
'8' => ("▄▀▀▄", "▄▀▀▄", "▀▄▄▀"),
'9' => ("▄▀▀▄", "▀▄▄█", " ▄▄▀"),
'.' => (" ", " ", "█"),
_ => continue,
};
lines.0.push_str(char_lines.0);
lines.1.push_str(char_lines.1);
lines.2.push_str(char_lines.2);
lines.0.push_str(" ");
lines.1.push_str(" ");
lines.2.push_str(" ");
}
lines
}
Loading

0 comments on commit 223a242

Please sign in to comment.