use num_bigint::BigUint;
use num_traits::One;
use serde::Serialize;
use std::fmt;
use std::ops::RangeInclusive;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Chain {
Bitcoin,
Ethereum,
Litecoin,
Monero,
Decred,
Arweave,
}
impl Chain {
pub const ALL: [Chain; 6] = [
Chain::Bitcoin,
Chain::Ethereum,
Chain::Litecoin,
Chain::Monero,
Chain::Decred,
Chain::Arweave,
];
pub fn symbol(&self) -> &'static str {
match self {
Chain::Bitcoin => "BTC",
Chain::Ethereum => "ETH",
Chain::Litecoin => "LTC",
Chain::Monero => "XMR",
Chain::Decred => "DCR",
Chain::Arweave => "AR",
}
}
pub fn name(&self) -> &'static str {
match self {
Chain::Bitcoin => "Bitcoin",
Chain::Ethereum => "Ethereum",
Chain::Litecoin => "Litecoin",
Chain::Monero => "Monero",
Chain::Decred => "Decred",
Chain::Arweave => "Arweave",
}
}
pub fn tx_explorer_url(&self, txid: &str) -> String {
match self {
Chain::Bitcoin => format!("https://mempool.space/tx/{}", txid),
Chain::Ethereum => format!("https://etherscan.io/tx/{}", txid),
Chain::Litecoin => format!("https://blockchair.com/litecoin/transaction/{}", txid),
Chain::Monero => format!("https://xmrchain.net/tx/{}", txid),
Chain::Decred => format!("https://dcrdata.decred.org/tx/{}", txid),
Chain::Arweave => format!("https://viewblock.io/arweave/tx/{}", txid),
}
}
pub fn address_explorer_url(&self, address: &str) -> String {
match self {
Chain::Bitcoin => format!("https://mempool.space/address/{}", address),
Chain::Ethereum => format!("https://etherscan.io/address/{}", address),
Chain::Litecoin => format!("https://blockchair.com/litecoin/address/{}", address),
Chain::Monero => format!("https://xmrchain.net/search?value={}", address),
Chain::Decred => format!("https://dcrdata.decred.org/address/{}", address),
Chain::Arweave => format!("https://viewblock.io/arweave/address/{}", address),
}
}
pub fn is_valid_txid(&self, txid: &str) -> bool {
fn is_hex64(s: &str) -> bool {
s.len() == 64 && s.as_bytes().iter().all(|b: &u8| b.is_ascii_hexdigit())
}
fn is_base64url_43(s: &str) -> bool {
s.len() == 43
&& s.as_bytes()
.iter()
.all(|b| matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_'))
}
match self {
Chain::Ethereum => txid.starts_with("0x") && txid.len() == 66 && is_hex64(&txid[2..]),
Chain::Bitcoin | Chain::Litecoin | Chain::Monero | Chain::Decred => is_hex64(txid),
Chain::Arweave => is_base64url_43(txid),
}
}
}
impl fmt::Display for Chain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Chain::Bitcoin => "bitcoin",
Chain::Ethereum => "ethereum",
Chain::Litecoin => "litecoin",
Chain::Monero => "monero",
Chain::Decred => "decred",
Chain::Arweave => "arweave",
})
}
}
impl FromStr for Chain {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"bitcoin" | "btc" => Ok(Chain::Bitcoin),
"ethereum" | "eth" => Ok(Chain::Ethereum),
"litecoin" | "ltc" => Ok(Chain::Litecoin),
"monero" | "xmr" => Ok(Chain::Monero),
"decred" | "dcr" => Ok(Chain::Decred),
"arweave" | "ar" => Ok(Chain::Arweave),
_ => Err(format!(
"unknown chain: '{}'. expected: bitcoin, ethereum, litecoin, monero, decred, arweave (or symbol: btc, eth, ltc, xmr, dcr, ar)",
s
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Solved,
Unsolved,
Claimed,
Swept,
Expired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TransactionType {
Funding,
Increase,
Decrease,
Sweep,
Claim,
PubkeyReveal,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Transaction {
pub tx_type: TransactionType,
pub txid: Option<&'static str>,
pub date: Option<&'static str>,
pub amount: Option<f64>,
}
impl Status {
pub fn is_active(&self) -> bool {
matches!(self, Status::Unsolved)
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Status::Solved => "solved",
Status::Unsolved => "unsolved",
Status::Claimed => "claimed",
Status::Swept => "swept",
Status::Expired => "expired",
})
}
}
impl FromStr for Status {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"solved" => Ok(Status::Solved),
"unsolved" => Ok(Status::Unsolved),
"claimed" => Ok(Status::Claimed),
"swept" => Ok(Status::Swept),
"expired" => Ok(Status::Expired),
_ => Err(format!(
"unknown status: '{}'. expected: solved, unsolved, claimed, swept, expired",
s
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Address {
pub value: &'static str,
pub chain: Chain,
pub kind: &'static str,
pub hash160: Option<&'static str>,
pub witness_program: Option<&'static str>,
pub redeem_script: Option<RedeemScript>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PubkeyFormat {
Compressed,
Uncompressed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct EntropySource {
pub url: Option<&'static str>,
pub description: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub enum Passphrase {
Required,
Known(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Entropy {
pub hash: &'static str,
pub source: Option<EntropySource>,
pub passphrase: Option<Passphrase>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Seed {
pub phrase: Option<&'static str>,
pub path: Option<&'static str>,
pub xpub: Option<&'static str>,
pub entropy: Option<Entropy>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Share {
pub index: u8,
pub data: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Shares {
pub threshold: u8,
pub total: u8,
pub shares: &'static [Share],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Wif {
pub encrypted: Option<&'static str>,
pub decrypted: Option<&'static str>,
pub passphrase: Option<&'static str>,
pub salt: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Key {
pub hex: Option<&'static str>,
pub wif: Option<Wif>,
pub seed: Option<Seed>,
pub mini: Option<&'static str>,
pub bits: Option<u16>,
pub shares: Option<Shares>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct RedeemScript {
pub script: &'static str,
pub hash: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Assets {
pub puzzle: Option<&'static str>,
pub solver: Option<&'static str>,
pub hints: &'static [&'static str],
pub source_url: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub struct Profile {
pub name: &'static str,
pub url: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Author {
pub name: Option<&'static str>,
pub addresses: &'static [&'static str],
pub profiles: &'static [Profile],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Solver {
pub name: Option<&'static str>,
pub addresses: &'static [&'static str],
pub profiles: &'static [Profile],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Pubkey {
pub value: &'static str,
pub format: PubkeyFormat,
}
#[derive(Debug, Clone, Serialize)]
pub struct Puzzle {
pub id: &'static str,
pub chain: Chain,
pub address: Address,
pub status: Status,
pub pubkey: Option<Pubkey>,
pub key: Option<Key>,
pub prize: Option<f64>,
pub currency: Option<&'static str>,
pub start_date: Option<&'static str>,
pub solve_date: Option<&'static str>,
pub solve_time: Option<u64>,
pub pre_genesis: bool,
pub source_url: Option<&'static str>,
pub transactions: &'static [Transaction],
pub solver: Option<Solver>,
pub assets: Option<Assets>,
}
fn format_duration_human_readable(seconds: u64) -> String {
const MINUTE: u64 = 60;
const HOUR: u64 = 60 * MINUTE;
const DAY: u64 = 24 * HOUR;
const MONTH: u64 = 30 * DAY;
const YEAR: u64 = 365 * DAY;
let years = seconds / YEAR;
let remaining = seconds % YEAR;
let months = remaining / MONTH;
let remaining = remaining % MONTH;
let days = remaining / DAY;
let remaining = remaining % DAY;
let hours = remaining / HOUR;
let remaining = remaining % HOUR;
let minutes = remaining / MINUTE;
let mut parts = Vec::new();
if years > 0 {
parts.push(format!("{}y", years));
}
if months > 0 {
parts.push(format!("{}mo", months));
}
if days > 0 {
parts.push(format!("{}d", days));
}
if hours > 0 {
parts.push(format!("{}h", hours));
}
if minutes > 0 {
parts.push(format!("{}m", minutes));
}
if parts.is_empty() {
format!("{}s", seconds)
} else {
parts.join(" ")
}
}
impl Key {
pub fn has_hex(&self) -> bool {
self.hex.is_some()
}
pub fn has_seed(&self) -> bool {
self.seed.is_some()
}
pub fn has_shares(&self) -> bool {
self.shares.is_some()
}
pub fn is_known(&self) -> bool {
self.hex.is_some() || self.wif.is_some() || self.seed.is_some() || self.mini.is_some()
}
pub fn range(&self) -> Option<RangeInclusive<u128>> {
let bits = self.bits?;
if !(1..=128).contains(&bits) {
return None;
}
let start = 1u128 << (bits - 1);
let end = if bits == 128 {
u128::MAX
} else {
(1u128 << bits) - 1
};
Some(start..=end)
}
pub fn range_big(&self) -> Option<(BigUint, BigUint)> {
let bits = self.bits?;
if !(1..=256).contains(&bits) {
return None;
}
let start = BigUint::one() << (bits - 1) as usize;
let end = (BigUint::one() << bits as usize) - 1u32;
Some((start, end))
}
}
impl Puzzle {
pub fn currency(&self) -> &'static str {
self.currency.unwrap_or_else(|| self.chain.symbol())
}
pub fn has_pubkey(&self) -> bool {
self.pubkey.is_some()
}
pub fn pubkey_str(&self) -> Option<&'static str> {
self.pubkey.map(|p| p.value)
}
pub fn has_private_key(&self) -> bool {
self.key.is_some_and(|k| k.is_known())
}
pub fn solve_time_formatted(&self) -> Option<String> {
self.solve_time.map(format_duration_human_readable)
}
pub fn collection(&self) -> &str {
self.id.split('/').next().unwrap_or(self.id)
}
pub fn name(&self) -> &str {
self.id.split('/').nth(1).unwrap_or("")
}
pub fn funding_tx(&self) -> Option<&Transaction> {
self.transactions
.iter()
.find(|t| t.tx_type == TransactionType::Funding)
}
pub fn claim_tx(&self) -> Option<&Transaction> {
self.transactions
.iter()
.find(|t| t.tx_type == TransactionType::Claim)
}
pub fn claim_txid(&self) -> Option<&'static str> {
self.claim_tx().and_then(|tx| tx.txid)
}
pub fn funding_txid(&self) -> Option<&'static str> {
self.funding_tx().and_then(|tx| tx.txid)
}
pub fn has_transactions(&self) -> bool {
!self.transactions.is_empty()
}
pub fn transaction_count(&self) -> usize {
self.transactions.len()
}
pub fn asset_path(&self) -> Option<String> {
self.assets
.and_then(|a| a.puzzle)
.map(|p| format!("assets/{}/{}", self.collection(), p))
}
pub fn asset_url(&self) -> Option<String> {
self.asset_path()
.map(|p| format!("https://raw.githubusercontent.com/oritwoen/boha/main/{}", p))
}
pub fn explorer_url(&self) -> String {
debug_assert_eq!(self.chain, self.address.chain);
self.address.chain.address_explorer_url(self.address.value)
}
pub fn key_range(&self) -> Option<RangeInclusive<u128>> {
self.key.and_then(|k| k.range())
}
pub fn key_range_big(&self) -> Option<(BigUint, BigUint)> {
self.key.and_then(|k| k.range_big())
}
}
pub trait IntoPuzzleNum {
fn into_puzzle_num(self) -> Option<u32>;
}
impl IntoPuzzleNum for u32 {
fn into_puzzle_num(self) -> Option<u32> {
Some(self)
}
}
impl IntoPuzzleNum for i32 {
fn into_puzzle_num(self) -> Option<u32> {
if self > 0 {
Some(self as u32)
} else {
None
}
}
}
impl IntoPuzzleNum for usize {
fn into_puzzle_num(self) -> Option<u32> {
u32::try_from(self).ok()
}
}
impl IntoPuzzleNum for &str {
fn into_puzzle_num(self) -> Option<u32> {
self.parse().ok()
}
}
impl IntoPuzzleNum for String {
fn into_puzzle_num(self) -> Option<u32> {
self.parse().ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_is_active() {
assert!(Status::Unsolved.is_active());
assert!(!Status::Solved.is_active());
assert!(!Status::Claimed.is_active());
assert!(!Status::Swept.is_active());
}
#[test]
fn chain_display_matches_serde() {
assert_eq!(Chain::Bitcoin.to_string(), "bitcoin");
assert_eq!(Chain::Ethereum.to_string(), "ethereum");
assert_eq!(Chain::Litecoin.to_string(), "litecoin");
assert_eq!(Chain::Monero.to_string(), "monero");
assert_eq!(Chain::Decred.to_string(), "decred");
assert_eq!(Chain::Arweave.to_string(), "arweave");
}
#[test]
fn chain_fromstr_by_name() {
assert_eq!("bitcoin".parse::<Chain>().unwrap(), Chain::Bitcoin);
assert_eq!("Bitcoin".parse::<Chain>().unwrap(), Chain::Bitcoin);
assert_eq!("ETHEREUM".parse::<Chain>().unwrap(), Chain::Ethereum);
}
#[test]
fn chain_fromstr_by_symbol() {
assert_eq!("btc".parse::<Chain>().unwrap(), Chain::Bitcoin);
assert_eq!("ETH".parse::<Chain>().unwrap(), Chain::Ethereum);
assert_eq!("ltc".parse::<Chain>().unwrap(), Chain::Litecoin);
assert_eq!("xmr".parse::<Chain>().unwrap(), Chain::Monero);
assert_eq!("dcr".parse::<Chain>().unwrap(), Chain::Decred);
assert_eq!("ar".parse::<Chain>().unwrap(), Chain::Arweave);
}
#[test]
fn chain_fromstr_invalid() {
assert!("dogecoin".parse::<Chain>().is_err());
assert!("".parse::<Chain>().is_err());
}
#[test]
fn status_display_matches_serde() {
assert_eq!(Status::Solved.to_string(), "solved");
assert_eq!(Status::Unsolved.to_string(), "unsolved");
assert_eq!(Status::Claimed.to_string(), "claimed");
assert_eq!(Status::Swept.to_string(), "swept");
}
#[test]
fn status_fromstr() {
assert_eq!("solved".parse::<Status>().unwrap(), Status::Solved);
assert_eq!("Unsolved".parse::<Status>().unwrap(), Status::Unsolved);
assert_eq!("CLAIMED".parse::<Status>().unwrap(), Status::Claimed);
assert_eq!("swept".parse::<Status>().unwrap(), Status::Swept);
}
#[test]
fn status_fromstr_invalid() {
assert!("pending".parse::<Status>().is_err());
assert!("".parse::<Status>().is_err());
}
#[test]
fn chain_roundtrip() {
for chain in Chain::ALL {
let s = chain.to_string();
assert_eq!(s.parse::<Chain>().unwrap(), chain);
}
}
#[test]
fn test_into_puzzle_num_i32() {
assert_eq!((-1i32).into_puzzle_num(), None);
assert_eq!(0i32.into_puzzle_num(), None);
assert_eq!(1i32.into_puzzle_num(), Some(1));
}
#[test]
fn valid_bitcoin_txid() {
let txid = "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d";
assert!(Chain::Bitcoin.is_valid_txid(txid));
}
#[test]
fn bitcoin_txid_accepts_mixed_case_hex() {
let lower = "a3b5c7d9e1f20000000000000000000000000000000000000000000000000001";
let upper = "A3B5C7D9E1F20000000000000000000000000000000000000000000000000001";
let mixed = "a3B5c7D9e1F20000000000000000000000000000000000000000000000000001";
assert!(Chain::Bitcoin.is_valid_txid(lower));
assert!(Chain::Bitcoin.is_valid_txid(upper));
assert!(Chain::Bitcoin.is_valid_txid(mixed));
}
#[test]
fn bitcoin_txid_rejects_wrong_length() {
assert!(!Chain::Bitcoin.is_valid_txid("abcd"));
assert!(!Chain::Bitcoin.is_valid_txid(""));
let too_long = "a".repeat(65);
assert!(!Chain::Bitcoin.is_valid_txid(&too_long));
}
#[test]
fn bitcoin_txid_rejects_non_hex() {
let txid = "g1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d";
assert!(!Chain::Bitcoin.is_valid_txid(txid));
}
#[test]
fn valid_ethereum_txid() {
let txid = "0xa1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d";
assert!(Chain::Ethereum.is_valid_txid(txid));
}
#[test]
fn ethereum_txid_requires_0x_prefix() {
let txid = "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d";
assert!(!Chain::Ethereum.is_valid_txid(txid));
}
#[test]
fn ethereum_txid_rejects_wrong_length() {
assert!(!Chain::Ethereum.is_valid_txid("0xabcd"));
assert!(!Chain::Ethereum.is_valid_txid("0x"));
}
#[test]
fn ethereum_txid_rejects_non_hex() {
let txid = "0xg1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d";
assert!(!Chain::Ethereum.is_valid_txid(txid));
}
#[test]
fn valid_arweave_txid() {
let txid = "hKMMPNh_emBf8v_at1tFzNYACisyMQNcKzeeE1QE9p8";
assert!(Chain::Arweave.is_valid_txid(txid));
}
#[test]
fn arweave_txid_rejects_wrong_length() {
assert!(!Chain::Arweave.is_valid_txid("too_short"));
assert!(!Chain::Arweave.is_valid_txid(""));
let too_long = "a".repeat(44);
assert!(!Chain::Arweave.is_valid_txid(&too_long));
}
#[test]
fn arweave_txid_rejects_invalid_chars() {
let txid = "hKMMPNh_emBf8v_at1tFzNYACisyMQNcKzeeE1QE9p!";
assert!(!Chain::Arweave.is_valid_txid(txid));
}
#[test]
fn litecoin_decred_and_monero_share_bitcoin_format() {
let txid = "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d";
assert!(Chain::Litecoin.is_valid_txid(txid));
assert!(Chain::Decred.is_valid_txid(txid));
assert!(Chain::Monero.is_valid_txid(txid));
}
#[test]
fn format_duration_zero_seconds() {
assert_eq!(format_duration_human_readable(0), "0s");
}
#[test]
fn format_duration_under_minute() {
assert_eq!(format_duration_human_readable(45), "45s");
}
#[test]
fn format_duration_exact_minute() {
assert_eq!(format_duration_human_readable(60), "1m");
}
#[test]
fn format_duration_hours_and_minutes() {
assert_eq!(format_duration_human_readable(3661), "1h 1m");
}
#[test]
fn format_duration_days() {
assert_eq!(format_duration_human_readable(86400), "1d");
}
#[test]
fn format_duration_months() {
assert_eq!(format_duration_human_readable(30 * 86400), "1mo");
}
#[test]
fn format_duration_years_and_months() {
let one_year_two_months = 365 * 86400 + 2 * 30 * 86400;
assert_eq!(
format_duration_human_readable(one_year_two_months),
"1y 2mo"
);
}
#[test]
fn format_duration_all_units() {
let duration = 365 * 86400 + 30 * 86400 + 86400 + 3600 + 60;
assert_eq!(format_duration_human_readable(duration), "1y 1mo 1d 1h 1m");
}
#[test]
fn tx_explorer_url_bitcoin() {
let url = Chain::Bitcoin.tx_explorer_url("abc123");
assert_eq!(url, "https://mempool.space/tx/abc123");
}
#[test]
fn tx_explorer_url_ethereum() {
let url = Chain::Ethereum.tx_explorer_url("0xabc");
assert_eq!(url, "https://etherscan.io/tx/0xabc");
}
#[test]
fn tx_explorer_url_litecoin() {
let url = Chain::Litecoin.tx_explorer_url("abc");
assert_eq!(url, "https://blockchair.com/litecoin/transaction/abc");
}
#[test]
fn tx_explorer_url_monero() {
let url = Chain::Monero.tx_explorer_url("abc");
assert_eq!(url, "https://xmrchain.net/tx/abc");
}
#[test]
fn tx_explorer_url_decred() {
let url = Chain::Decred.tx_explorer_url("abc");
assert_eq!(url, "https://dcrdata.decred.org/tx/abc");
}
#[test]
fn tx_explorer_url_arweave() {
let url = Chain::Arweave.tx_explorer_url("abc");
assert_eq!(url, "https://viewblock.io/arweave/tx/abc");
}
#[test]
fn chain_symbol() {
assert_eq!(Chain::Bitcoin.symbol(), "BTC");
assert_eq!(Chain::Ethereum.symbol(), "ETH");
assert_eq!(Chain::Litecoin.symbol(), "LTC");
assert_eq!(Chain::Monero.symbol(), "XMR");
assert_eq!(Chain::Decred.symbol(), "DCR");
assert_eq!(Chain::Arweave.symbol(), "AR");
}
#[test]
fn chain_name() {
assert_eq!(Chain::Bitcoin.name(), "Bitcoin");
assert_eq!(Chain::Ethereum.name(), "Ethereum");
assert_eq!(Chain::Litecoin.name(), "Litecoin");
assert_eq!(Chain::Monero.name(), "Monero");
assert_eq!(Chain::Decred.name(), "Decred");
assert_eq!(Chain::Arweave.name(), "Arweave");
}
#[test]
fn chain_all_contains_every_variant() {
assert_eq!(Chain::ALL.len(), 6);
assert!(Chain::ALL.contains(&Chain::Bitcoin));
assert!(Chain::ALL.contains(&Chain::Ethereum));
assert!(Chain::ALL.contains(&Chain::Litecoin));
assert!(Chain::ALL.contains(&Chain::Monero));
assert!(Chain::ALL.contains(&Chain::Decred));
assert!(Chain::ALL.contains(&Chain::Arweave));
}
#[test]
fn test_address_explorer_url_all_chains() {
let cases: &[(Chain, &str, &str)] = &[
(Chain::Bitcoin, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "https://mempool.space/address/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"),
(Chain::Ethereum, "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", "https://etherscan.io/address/0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae"),
(Chain::Litecoin, "LVuDpNCSSj6pQ7t9Pv6d6sUkLKoqDEVUnJ", "https://blockchair.com/litecoin/address/LVuDpNCSSj6pQ7t9Pv6d6sUkLKoqDEVUnJ"),
(Chain::Monero, "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A", "https://xmrchain.net/search?value=44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A"),
(Chain::Decred, "DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu", "https://dcrdata.decred.org/address/DsUZxxoHJSty8DCfwfartwTYbuhmVct7tJu"),
(Chain::Arweave, "vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI", "https://viewblock.io/arweave/address/vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI"),
];
for (chain, addr, expected) in cases {
assert_eq!(
chain.address_explorer_url(addr),
*expected,
"failed for {:?}",
chain
);
}
}
#[test]
fn test_puzzle_explorer_url_delegates() {
let puzzle = crate::b1000::get(1).expect("puzzle b1000/1 should exist");
let expected = puzzle.chain.address_explorer_url(puzzle.address.value);
assert_eq!(puzzle.explorer_url(), expected);
}
#[test]
fn currency_defaults_to_chain_symbol() {
let puzzle = crate::b1000::get(1).expect("puzzle b1000/1 should exist");
assert!(puzzle.currency.is_none());
assert_eq!(puzzle.currency(), "BTC");
}
#[test]
fn currency_returns_explicit_value() {
let puzzle = crate::b1000::get(1).expect("puzzle b1000/1 should exist");
assert_eq!(puzzle.currency(), puzzle.chain.symbol());
assert_eq!(Some("DAI").unwrap_or_else(|| "BTC"), "DAI");
}
}