use crate::error::{Result, TransportError};
use crate::transport::ticket::Ticket;
use pkarr::{Client, Keypair, SignedPacket, dns::rdata::RData, dns::rdata::TXT};
use sha2::{Digest, Sha256};
use std::time::Duration;
use tracing::{debug, info, warn};
const WORMHOLE_PKARR_SALT: &[u8] = b"irosh-wormhole-v1";
pub const PAIRING_ALPN: &[u8] = b"irosh/pairing/v1";
pub fn derive_keypair(code: &str) -> Keypair {
let mut hasher = Sha256::new();
hasher.update(WORMHOLE_PKARR_SALT);
hasher.update(code.as_bytes());
let seed: [u8; 32] = hasher.finalize().into();
Keypair::from_secret_key(&seed)
}
pub async fn listen_for_ticket(_endpoint: &iroh::Endpoint, code: &str) -> Result<Ticket> {
let keypair = derive_keypair(code);
let public_key = keypair.public_key();
let client = Client::builder()
.build()
.map_err(|e| TransportError::ProtocolError {
details: format!("Failed to build pkarr client: {}", e),
})?;
info!("Searching for wormhole rendezvous via Pkarr: {}", code);
for i in 0..60 {
if let Some(signed_packet) = client.resolve(&public_key).await {
for record in signed_packet.all_resource_records() {
if let RData::TXT(txt) = &record.rdata {
if let Ok(content) = String::try_from(txt.clone()) {
if let Some(ticket_str) = content.strip_prefix("irosh-ticket=") {
if let Ok(ticket) = ticket_str.parse::<Ticket>() {
info!("Wormhole discovered via Pkarr rendezvous");
return Ok(ticket);
}
}
}
}
}
}
if i % 6 == 0 && i > 0 {
info!("... still searching for wormhole: {} (attempt {})", code, i);
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
Err(TransportError::ProtocolError {
details: "Timed out waiting for wormhole discovery".to_string(),
}
.into())
}
pub async fn broadcast_ticket_loop(
_gossip: &iroh_gossip::net::Gossip, code: &str,
ticket: Ticket,
) -> Result<()> {
let keypair = derive_keypair(code);
let client = Client::builder()
.build()
.map_err(|e| TransportError::ProtocolError {
details: format!("Failed to build pkarr client: {}", e),
})?;
let msg = format!("irosh-ticket={}", ticket);
let txt = TXT::try_from(msg.as_str()).map_err(|e| TransportError::ProtocolError {
details: format!("Failed to create TXT record: {}", e),
})?;
let signed_packet = SignedPacket::builder()
.txt(
"_irosh"
.try_into()
.map_err(|_| TransportError::ProtocolError {
details: "Failed to convert _irosh string to Pkarr name".to_string(),
})?,
txt,
300,
)
.sign(&keypair)
.map_err(|e| TransportError::ProtocolError {
details: format!("Failed to sign pkarr packet: {}", e),
})?;
info!("Publishing wormhole to Pkarr rendezvous: {}", code);
loop {
match client.publish(&signed_packet, None).await {
Ok(_) => debug!("Successfully published to Pkarr rendezvous"),
Err(e) => warn!("Failed to publish to Pkarr rendezvous: {}", e),
}
tokio::time::sleep(Duration::from_secs(60)).await;
}
}
pub async fn unpublish_ticket(code: &str) -> Result<()> {
let keypair = derive_keypair(code);
let client = Client::builder()
.build()
.map_err(|e| TransportError::ProtocolError {
details: format!("Failed to build pkarr client: {}", e),
})?;
let signed_packet =
SignedPacket::builder()
.sign(&keypair)
.map_err(|e| TransportError::ProtocolError {
details: format!("Failed to sign empty pkarr packet: {}", e),
})?;
debug!("Unpublishing wormhole from Pkarr: {}", code);
let _ = client.publish(&signed_packet, None).await;
Ok(())
}
pub fn generate_code() -> String {
use rand::Rng;
let mut rng = rand::rng();
const WORDS: &[&str] = &[
"apple", "banana", "cherry", "dog", "elephant", "fox", "grape", "honey", "iron", "jungle",
"kite", "lemon", "mountain", "night", "ocean", "piano", "quartz", "river", "sky", "tiger",
"umbrella", "valley", "whale", "xray", "yellow", "zebra", "amber", "bright", "crystal",
"delta", "echo", "frost",
];
let w1 = WORDS[rng.random_range(0..WORDS.len())];
let w2 = WORDS[rng.random_range(0..WORDS.len())];
let n = rng.random_range(1..10);
format!("{}-{}-{}", w1, w2, n)
}