use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use paralock::{Hex, ZcashNode, ZcashRpc};
use paralock_core::keys::{KeyPair, PrivateKey};
use paralock_core::zcash::{Network, TransparentAddress};
use serde::Deserialize;
use zeroize::Zeroizing;
use crate::error::{CliError, CliResult};
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DevnetFile {
pub zcash: DevnetZcash,
pub evm: BTreeMap<String, DevnetEvm>,
pub maker: DevnetAccount,
pub taker: DevnetAccount,
pub relayer: DevnetRelayer,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DevnetZcash {
pub rpc: String,
#[serde(default)]
pub lightwalletd: Option<String>,
pub dashboard: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DevnetEvm {
pub rpc: String,
pub chain_id: u64,
pub htlc: String,
#[serde(default)]
pub adaptor_escrow: Option<String>,
pub tokens: BTreeMap<String, String>,
pub quote_token: String,
#[serde(default)]
pub start_block: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DevnetAccount {
#[serde(default)]
pub url: Option<String>,
pub evm_key: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DevnetRelayer {
pub evm_key: String,
}
impl DevnetEvm {
pub fn quote_token_address(&self) -> CliResult<Hex> {
let address = self
.tokens
.get(&self.quote_token)
.ok_or_else(|| CliError::usage(format!("devnet.json lists no {} token", self.quote_token)))?;
Hex::new(address).map_err(|_| CliError::usage(format!("devnet.json: bad {} address", self.quote_token)))
}
}
pub fn read_devnet(state_dir: &Path) -> CliResult<(PathBuf, DevnetFile)> {
let path = state_dir.join("devnet.json");
let text = Zeroizing::new(std::fs::read_to_string(&path).map_err(|_| {
CliError::usage(format!("no devnet found at {}", path.display())).hint(
"start one with `npx paralock dev` (the npm CLI runs the devnet; this binary only uses it), or set \
PARALOCK_HOME to the directory that holds devnet.json",
)
})?);
let file: DevnetFile = serde_json::from_str(&text).map_err(|e| {
CliError::usage(format!("{} is not a devnet.json this version understands: {e}", path.display()))
.hint("run `npx paralock devnet up` again")
})?;
Ok((path, file))
}
pub const ANVIL_KEYS: [&str; 10] = [
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
"0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
"0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
"0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
"0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
"0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
"0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
];
pub fn load_or_create_key(path: &Path) -> CliResult<Zeroizing<String>> {
if let Ok(text) = std::fs::read_to_string(path) {
let key = Zeroizing::new(text.trim().to_owned());
PrivateKey::from_hex(key.strip_prefix("0x").unwrap_or(&key))
.map_err(|_| CliError::usage(format!("{} does not hold a private key", path.display())))?;
return Ok(key);
}
if let Some(dir) = path.parent() {
create_private_dir(dir)?;
}
let key = KeyPair::try_random(&mut paralock_core::OsRng)
.map_err(|e| CliError::failed(format!("no randomness for a new key: {e}")))?;
let hex = Zeroizing::new(paralock_core::encoding::to_hex(&*key.private_key().to_bytes()));
write_private_file(path, hex.as_bytes())?;
Ok(hex)
}
pub fn create_private_dir(dir: &Path) -> CliResult<()> {
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
std::fs::DirBuilder::new().recursive(true).mode(0o700).create(dir)?;
}
#[cfg(not(unix))]
std::fs::create_dir_all(dir)?;
Ok(())
}
pub fn write_private_file(path: &Path, bytes: &[u8]) -> CliResult<()> {
use std::io::Write as _;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path)?;
file.write_all(bytes)?;
file.sync_all()?;
Ok(())
}
pub fn address_of(key: &str) -> CliResult<String> {
let key = PrivateKey::from_hex(key.strip_prefix("0x").unwrap_or(key))
.map_err(|_| CliError::usage("not a 32-byte secp256k1 private key"))?;
Ok(TransparentAddress::p2pkh(&key.public_key(), Network::Regtest).encode())
}
#[derive(Debug, Clone)]
pub struct Faucet {
url: String,
client: reqwest::Client,
}
const FAUCET_MAX_ZAT: u64 = 500_000_000;
impl Faucet {
pub fn new(url: &str) -> CliResult<Self> {
let client = reqwest::Client::builder()
.user_agent(paralock::DEFAULT_USER_AGENT)
.timeout(Duration::from_secs(120))
.build()
.map_err(|e| CliError::failed(format!("HTTP client: {e}")))?;
Ok(Self { url: url.trim_end_matches('/').to_owned(), client })
}
pub async fn pay(&self, address: &str, mut zat: u64) -> CliResult<()> {
while zat > 0 {
let amount = zat.min(FAUCET_MAX_ZAT);
self.request(address, amount).await?;
zat -= amount;
}
Ok(())
}
async fn request(&self, address: &str, amount: u64) -> CliResult<()> {
let mut key = [0u8; 8];
rand_core::RngCore::fill_bytes(&mut paralock_core::OsRng, &mut key);
let body = serde_json::json!({
"address": address,
"amount_zatoshi": amount,
"idempotency_key": paralock_core::encoding::to_hex(&key),
});
let mut last = String::new();
for attempt in 0..8u32 {
if attempt > 0 {
tokio::time::sleep(Duration::from_millis(750) * attempt).await;
}
let response = self
.client
.post(format!("{}/api/v1/faucet/address", self.url))
.header("content-type", "application/json")
.body(body.to_string())
.send()
.await;
match response {
Ok(r) if r.status().is_success() => return Ok(()),
Ok(r) => last = format!("HTTP {}: {}", r.status().as_u16(), r.text().await.unwrap_or_default()),
Err(e) => last = e.without_url().to_string(),
}
}
Err(CliError::failed(format!("the ths faucet failed: {}", last.trim()))
.hint("is the devnet running? `npx paralock devnet status`"))
}
}
pub struct Miner {
node: ZcashRpc,
last: Mutex<(u32, Instant)>,
}
const STALL: Duration = Duration::from_secs(6);
impl Miner {
pub fn new(rpc: &str) -> CliResult<Self> {
let node = ZcashRpc::new(rpc).map_err(CliError::from)?;
Ok(Self { node, last: Mutex::new((0, Instant::now())) })
}
pub async fn tick(&self) -> bool {
let Ok(tip) = self.node.get_block_count().await else {
tokio::time::sleep(Duration::from_secs(1)).await;
return false;
};
let stalled = {
let mut last = self.last.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if last.0 != tip {
*last = (tip, Instant::now());
}
last.1.elapsed() >= STALL
};
if !stalled {
tokio::time::sleep(Duration::from_secs(1)).await;
return false;
}
let mined = self.node.generate(1).await.is_ok();
if mined {
let mut last = self.last.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
*last = (tip.saturating_add(1), Instant::now());
}
mined
}
}