use crate::script::locking_script::LockingScript;
use crate::services::ServicesError;
#[derive(Debug, Clone, PartialEq)]
pub struct OverlayAdminTokenTemplate {
pub protocol: String,
pub identity_key: String,
pub domain: String,
pub topic_or_service: String,
}
impl OverlayAdminTokenTemplate {
pub fn new(
protocol: &str,
identity_key: &str,
domain: &str,
topic_or_service: &str,
) -> Result<Self, ServicesError> {
if protocol != "SHIP" && protocol != "SLAP" {
return Err(ServicesError::Overlay(format!(
"Invalid protocol: {} (expected SHIP or SLAP)",
protocol
)));
}
Ok(OverlayAdminTokenTemplate {
protocol: protocol.to_string(),
identity_key: identity_key.to_string(),
domain: domain.to_string(),
topic_or_service: topic_or_service.to_string(),
})
}
pub fn encode_fields(&self) -> Vec<Vec<u8>> {
vec![
self.protocol.as_bytes().to_vec(),
hex_decode(&self.identity_key).unwrap_or_default(),
self.domain.as_bytes().to_vec(),
self.topic_or_service.as_bytes().to_vec(),
]
}
pub fn decode(script: &LockingScript) -> Result<Self, ServicesError> {
let pd = crate::script::templates::PushDrop::decode(script)
.map_err(|e| ServicesError::Overlay(format!("PushDrop decode failed: {e}")))?;
let fields = pd.fields;
if fields.len() < 4 {
return Err(ServicesError::Overlay(
"Invalid SHIP/SLAP advertisement: fewer than 4 fields".to_string(),
));
}
let protocol = String::from_utf8(fields[0].clone())
.map_err(|_| ServicesError::Overlay("Invalid protocol field UTF-8".to_string()))?;
if protocol != "SHIP" && protocol != "SLAP" {
return Err(ServicesError::Overlay(format!(
"Invalid protocol type: {}",
protocol
)));
}
let identity_key = hex_encode(&fields[1]);
let domain = String::from_utf8(fields[2].clone())
.map_err(|_| ServicesError::Overlay("Invalid domain field UTF-8".to_string()))?;
let topic_or_service = String::from_utf8(fields[3].clone()).map_err(|_| {
ServicesError::Overlay("Invalid topicOrService field UTF-8".to_string())
})?;
Ok(OverlayAdminTokenTemplate {
protocol,
identity_key,
domain,
topic_or_service,
})
}
pub fn decode_from_beef(tx_bytes: &[u8], output_index: usize) -> Result<Self, ServicesError> {
let beef_hex: String = tx_bytes.iter().map(|b| format!("{:02x}", b)).collect();
let tx = crate::transaction::Transaction::from_beef(&beef_hex)
.or_else(|_| {
crate::transaction::Transaction::from_binary(&mut &tx_bytes[..])
})
.map_err(|e| ServicesError::Overlay(format!("Failed to parse transaction: {}", e)))?;
let output = tx.outputs.get(output_index).ok_or_else(|| {
ServicesError::Overlay(format!(
"Output index {} out of range (tx has {} outputs)",
output_index,
tx.outputs.len()
))
})?;
Self::decode(&output.locking_script)
}
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn hex_decode(hex: &str) -> Result<Vec<u8>, ServicesError> {
if !hex.len().is_multiple_of(2) {
return Err(ServicesError::Serialization(
"hex string has odd length".to_string(),
));
}
(0..hex.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&hex[i..i + 2], 16)
.map_err(|_| ServicesError::Serialization("invalid hex character".to_string()))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::private_key::PrivateKey;
use crate::script::templates::push_drop::PushDrop;
use crate::script::templates::ScriptTemplateLock;
#[test]
fn test_encode_decode_round_trip() {
let template = OverlayAdminTokenTemplate::new(
"SLAP",
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"https://overlay.example.com",
"ls_test_service",
)
.unwrap();
let key = PrivateKey::from_hex("1").unwrap();
let pd = PushDrop::new(template.encode_fields(), key);
let lock_script = pd.lock().unwrap();
let decoded = OverlayAdminTokenTemplate::decode(&lock_script).unwrap();
assert_eq!(decoded.protocol, "SLAP");
assert_eq!(
decoded.identity_key,
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
);
assert_eq!(decoded.domain, "https://overlay.example.com");
assert_eq!(decoded.topic_or_service, "ls_test_service");
}
#[test]
fn test_encode_decode_ship_protocol() {
let template = OverlayAdminTokenTemplate::new(
"SHIP",
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"https://host.example.com",
"tm_test_topic",
)
.unwrap();
let key = PrivateKey::from_hex("1").unwrap();
let pd = PushDrop::new(template.encode_fields(), key);
let lock_script = pd.lock().unwrap();
let decoded = OverlayAdminTokenTemplate::decode(&lock_script).unwrap();
assert_eq!(decoded, template);
}
#[test]
fn test_invalid_protocol_rejected() {
let result = OverlayAdminTokenTemplate::new("INVALID", "key", "domain", "service");
assert!(result.is_err());
}
#[test]
fn test_hex_encode_decode_round_trip() {
let original = vec![0xab, 0xcd, 0xef, 0x01];
let hex = hex_encode(&original);
let decoded = hex_decode(&hex).unwrap();
assert_eq!(decoded, original);
}
#[tokio::test]
#[ignore] async fn test_decode_production_slap_response() {
let client = reqwest::Client::new();
let resp = client
.post("https://overlay-us-1.bsvb.tech/lookup")
.json(&serde_json::json!({"service":"ls_slap","query":{"service":"ls_ship"}}))
.send()
.await
.expect("SLAP query");
let data: serde_json::Value = resp.json().await.expect("parse JSON");
let outputs = data["outputs"].as_array().expect("outputs array");
assert!(!outputs.is_empty(), "should have SLAP outputs");
let mut parsed_count = 0;
for out in outputs {
let beef: Vec<u8> = out["beef"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as u8)
.collect();
let idx = out["outputIndex"].as_u64().unwrap() as usize;
match OverlayAdminTokenTemplate::decode_from_beef(&beef, idx) {
Ok(parsed) => {
eprintln!(
" Parsed: {} {} {}",
parsed.protocol, parsed.domain, parsed.topic_or_service
);
parsed_count += 1;
}
Err(e) => {
eprintln!(
" FAILED output idx={}: {} (beef len={}, first 20 bytes={:?})",
idx,
e,
beef.len(),
&beef[..20.min(beef.len())]
);
}
}
}
assert!(parsed_count > 0, "should parse at least one SLAP entry");
}
}