use std::path::Path;
use thiserror::Error;
use tape_api::state::Group;
use tape_rpc_client::RpcClient;
use tape_rpc_solana::{RpcConfig, SolanaRpc};
use tape_core::bls::BlsPrivateKey;
use tape_core::system::Member;
use tape_core::types::SpoolIndex;
use tape_crypto::Address;
use tape_crypto::ed25519::errors::KeypairFileError;
use tape_crypto::ed25519::Keypair;
use tape_crypto::Hash;
#[derive(Debug, Error)]
pub enum HelperError {
#[error(transparent)]
KeypairFile(#[from] KeypairFileError),
#[error("Failed to read {path}: {message}")]
FileRead { path: String, message: String },
#[error("Invalid JSON in {path}: {message}")]
JsonParse { path: String, message: String },
#[error("Invalid {name} hex: {message}")]
InvalidHex { name: String, message: String },
#[error("{name} must be {expected} bytes (got {actual})")]
InvalidLength { name: String, expected: usize, actual: usize },
#[error("Node {0} not found in committee")]
NodeNotInCommittee(Address),
#[error("Failed to create RPC client: {0}")]
RpcClient(String),
}
pub fn load_solana_keypair(
path: &Path,
) -> Result<solana_keypair::Keypair, HelperError> {
let keypair = Keypair::try_load_json_file(path)?;
keypair.try_to_solana_keypair().map_err(|error| {
KeypairFileError::InvalidKeypair(error.to_string()).into()
})
}
pub fn load_ed25519_keypair(path: &Path) -> Result<Keypair, HelperError> {
Keypair::try_load_json_file(path).map_err(Into::into)
}
pub fn load_ed25519_keypair_bytes(bytes: &[u8]) -> Result<Keypair, HelperError> {
Keypair::try_from_json_bytes(bytes).map_err(Into::into)
}
pub fn load_bls_keypair(path: &Path) -> Result<BlsPrivateKey, HelperError> {
let contents = std::fs::read(path).map_err(|e| HelperError::FileRead {
path: path.display().to_string(),
message: e.to_string(),
})?;
let bytes: Vec<u8> = serde_json::from_slice(&contents).map_err(|e| HelperError::JsonParse {
path: path.display().to_string(),
message: e.to_string(),
})?;
if bytes.len() != 32 {
return Err(HelperError::InvalidLength {
name: "BLS keypair".to_string(),
expected: 32,
actual: bytes.len(),
});
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
let secret = tape_crypto::bls12254::min_sig::PrivKey::try_from_bytes(arr).map_err(|_| {
KeypairFileError::InvalidKeypair("invalid BLS private key scalar".to_string())
})?;
Ok(BlsPrivateKey(secret))
}
pub fn ensure_ed25519_keypair(path: &Path) -> Result<Keypair, HelperError> {
if path.exists() {
return load_ed25519_keypair(path);
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(|e| HelperError::FileRead {
path: parent.display().to_string(),
message: e.to_string(),
})?;
}
}
let mut rng = rand::thread_rng();
let keypair = Keypair::new(&mut rng);
let bytes: [u8; 64] = keypair.to_keypair_bytes();
let json = serde_json::to_vec(&bytes.to_vec()).map_err(|e| HelperError::JsonParse {
path: path.display().to_string(),
message: e.to_string(),
})?;
std::fs::write(path, &json).map_err(|e| HelperError::FileRead {
path: path.display().to_string(),
message: e.to_string(),
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
let _ = std::fs::set_permissions(path, perms);
}
Ok(keypair)
}
pub fn parse_hash(hex_str: &str, name: &str) -> Result<Hash, HelperError> {
let bytes = hex::decode(hex_str).map_err(|e| HelperError::InvalidHex {
name: name.to_string(),
message: e.to_string(),
})?;
if bytes.len() != 32 {
return Err(HelperError::InvalidLength {
name: name.to_string(),
expected: 32,
actual: bytes.len(),
});
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(Hash::from(arr))
}
pub fn parse_hex_bytes(hex_str: &str, name: &str, expected_len: usize) -> Result<Vec<u8>, HelperError> {
let bytes = hex::decode(hex_str).map_err(|e| HelperError::InvalidHex {
name: name.to_string(),
message: e.to_string(),
})?;
if bytes.len() != expected_len {
return Err(HelperError::InvalidLength {
name: name.to_string(),
expected: expected_len,
actual: bytes.len(),
});
}
Ok(bytes)
}
pub fn find_member_index(committee: &[Member], node: Address) -> Option<usize> {
committee.iter().position(|m| m.node == node)
}
pub fn get_node_assigned_spools(
groups: &[Group],
node: Address,
) -> Result<Vec<SpoolIndex>, HelperError> {
let spools = groups
.iter()
.flat_map(|group| {
group
.spools
.iter()
.enumerate()
.filter_map(move |(position, spool)| {
(spool.node == node).then_some(group.id.spool_at(position))
})
})
.collect::<Vec<_>>();
if spools.is_empty() {
return Err(HelperError::NodeNotInCommittee(node));
}
Ok(spools)
}
pub fn create_rpc_client(endpoint: &str) -> Result<RpcClient<SolanaRpc>, HelperError> {
let config = RpcConfig {
endpoints: vec![endpoint.to_string()],
..Default::default()
};
RpcClient::new(config).map_err(|e| HelperError::RpcClient(e.to_string()))
}
pub fn create_rpc_client_with_config(config: RpcConfig) -> Result<RpcClient<SolanaRpc>, HelperError> {
RpcClient::new(config).map_err(|e| HelperError::RpcClient(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_parse_hash_valid() {
let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let result = parse_hash(hex, "test");
assert!(result.is_ok());
}
#[test]
fn test_parse_hash_invalid_hex() {
let hex = "not_valid_hex";
let result = parse_hash(hex, "test");
assert!(matches!(result, Err(HelperError::InvalidHex { .. })));
}
#[test]
fn test_parse_hash_wrong_length() {
let hex = "0123456789abcdef"; let result = parse_hash(hex, "test");
assert!(matches!(result, Err(HelperError::InvalidLength { .. })));
}
#[test]
fn test_parse_hex_bytes() {
let hex = "ff00ff00";
let result = parse_hex_bytes(hex, "test", 4);
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec![0xff, 0x00, 0xff, 0x00]);
}
#[test]
fn load_bls_keypair_accepts_valid_scalar() {
let file = NamedTempFile::new().expect("temp file");
let mut bytes = vec![0u8; 32];
bytes[31] = 1;
std::fs::write(file.path(), serde_json::to_vec(&bytes).expect("json"))
.expect("write key");
let result = load_bls_keypair(file.path());
assert!(result.is_ok());
}
#[test]
fn load_bls_keypair_rejects_zero_scalar() {
let file = NamedTempFile::new().expect("temp file");
let bytes = vec![0u8; 32];
std::fs::write(file.path(), serde_json::to_vec(&bytes).expect("json"))
.expect("write key");
let result = load_bls_keypair(file.path());
assert!(matches!(
result,
Err(HelperError::KeypairFile(KeypairFileError::InvalidKeypair(_)))
));
}
}