#![doc = include_str!("../README.md")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use ciborium;
use brotli::{BrotliCompress, BrotliDecompress};
use brotli::enc::BrotliEncoderParams;
use log::{debug, error};
mod id;
pub use id::Id;
mod resource;
pub use resource::{Resource, ResourceValue};
mod wallet;
pub use wallet::{Wallet, WalletType};
mod dns;
pub use dns::Dns;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct SubDomain {
pub owner: String,
pub general: String,
pub twitter: String,
pub url: String,
pub nostr: String,
pub lightning: String,
pub btc: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Wallets {
pub wallets: HashMap<WalletType, String> }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "v")]
pub enum ZoneFile {
#[serde(rename = "0")]
V0 {
owner: String,
general: String,
twitter: String,
url: String,
nostr: String,
lightning: String,
btc: String,
subdomains: HashMap<String, SubDomain>,
},
#[serde(rename = "1")]
V1 {
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
resources: Option<HashMap<String, Resource>>,
#[serde(skip_serializing_if = "Option::is_none")]
wallets: Option<HashMap<WalletType, String>>,
#[serde(skip_serializing_if = "crate::dns::is_dns_none")]
dns: Option<Dns>,
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Service {
pub name: String,
pub proof: String,
}
impl ZoneFile {
pub fn from_str(zonefile_str: &str) -> Result<Self, serde_json::Error> {
debug!("Attempting to parse zonefile from string");
if let Ok(v1) = serde_json::from_str::<Self>(zonefile_str) {
debug!("Successfully parsed V1 zonefile");
return Ok(v1);
}
debug!("V1 parsing failed, attempting to parse as legacy V0");
let legacy: Result<LegacyZoneFile, _> = serde_json::from_str(zonefile_str);
match legacy {
Ok(v0) => {
debug!("Converting legacy V0 to proper V0 with version tag");
let v0_json = serde_json::json!({
"v": "0",
"owner": v0.owner,
"general": v0.general,
"twitter": v0.twitter,
"url": v0.url,
"nostr": v0.nostr,
"lightning": v0.lightning,
"btc": v0.btc,
"subdomains": v0.subdomains,
});
let result = serde_json::from_value(v0_json);
if result.is_ok() {
debug!("Successfully parsed and converted legacy V0 zonefile");
} else {
error!("Failed to convert legacy V0 to proper V0 format");
}
result
},
Err(e) => {
error!("Failed to parse zonefile: {}", e);
Err(e)
}
}
}
pub fn to_string(&self) -> Result<String, serde_json::Error> {
debug!("Converting zonefile to JSON string");
let result = serde_json::to_string(self);
if let Err(ref e) = result {
error!("Failed to convert zonefile to string: {}", e);
} else {
debug!("Successfully converted zonefile to string");
}
result
}
pub fn new_v1(default: &str) -> Self {
debug!("Creating new V1 zonefile with default: {}", default);
ZoneFile::V1 { default: Some(default.to_string()), id: None, resources: None, wallets: None, dns: None }
}
pub fn new_v0(
owner: String,
general: String,
twitter: String,
url: String,
nostr: String,
lightning: String,
btc: String,
subdomains: HashMap<String, SubDomain>,
) -> Self {
debug!("Creating new V0 zonefile for owner: {}", owner);
ZoneFile::V0 {
owner,
general,
twitter,
url,
nostr,
lightning,
btc,
subdomains,
}
}
pub fn from_clarity_buffer_hex_string(hex_string: &str) -> Result<Self, serde_json::Error> {
debug!("Parsing zonefile from clarity buffer hex string");
let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex_string);
let zonefile = ZoneFile::from_bytes(&bytes)?;
debug!("Successfully parsed zonefile from clarity buffer");
Ok(zonefile)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
debug!("Attempting to parse zonefile from {} bytes", bytes.len());
let mut decompressed = Vec::new();
let decompressed_result = {
let mut reader = bytes;
let mut writer = &mut decompressed;
BrotliDecompress(&mut reader, &mut writer)
};
let processed_bytes = match decompressed_result {
Ok(_) => {
debug!("Successfully decompressed brotli data");
decompressed
},
Err(e) => {
debug!("Brotli decompression failed ({}), using raw bytes", e);
bytes.to_vec()
}
};
debug!("Attempting to parse as CBOR");
match ZoneFile::from_cbor(&processed_bytes) {
Ok(zonefile) => {
debug!("Successfully parsed CBOR data");
Ok(zonefile)
},
Err(e) => {
debug!("CBOR parsing failed ({}), attempting JSON parsing", e);
match std::str::from_utf8(&processed_bytes) {
Ok(str_data) => {
debug!("Attempting to parse as JSON string");
let result = ZoneFile::from_str(str_data);
if result.is_ok() {
debug!("Successfully parsed JSON data");
} else {
error!("Failed to parse JSON data");
}
result
},
Err(e) => {
error!("Invalid UTF-8 in data: {}", e);
Err(serde_json::Error::io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid UTF-8: {}", e)
)))
}
}
}
}
}
pub fn from_cbor(cbor_bytes: &[u8]) -> Result<Self, ciborium::de::Error<std::io::Error>> {
debug!("Parsing zonefile from {} bytes of CBOR data", cbor_bytes.len());
let result = ciborium::de::from_reader(cbor_bytes);
if result.is_err() {
error!("Failed to parse CBOR data");
}
result
}
pub fn to_cbor(&self) -> Result<Vec<u8>, ciborium::ser::Error<std::io::Error>> {
debug!("Converting zonefile to CBOR");
let mut bytes = Vec::new();
let result = ciborium::ser::into_writer(self, &mut bytes);
if let Err(ref e) = result {
error!("Failed to convert to CBOR: {}", e);
} else {
debug!("Successfully converted to {} bytes of CBOR", bytes.len());
}
result.map(|_| bytes)
}
pub fn to_compressed_cbor(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
debug!("Converting zonefile to compressed CBOR");
let cbor_bytes = self.to_cbor()?;
let mut compressed = Vec::new();
{
let mut reader = &cbor_bytes[..];
let mut writer = &mut compressed;
let params = BrotliEncoderParams {
quality: 11, lgwin: 22, ..Default::default()
};
debug!("Compressing {} bytes of CBOR data", cbor_bytes.len());
match BrotliCompress(&mut reader, &mut writer, ¶ms) {
Ok(_) => {
debug!("Successfully compressed CBOR data: {} -> {} bytes", cbor_bytes.len(), compressed.len());
},
Err(e) => {
error!("Failed to compress CBOR data: {}", e);
return Err(Box::new(e));
}
}
}
Ok(compressed)
}
fn clarity_buffer_to_uint8_array(hex_string: &str) -> Vec<u8> {
debug!("Converting clarity buffer hex string to bytes");
let hex = hex_string.strip_prefix("0x").unwrap_or(hex_string);
let bytes: Vec<u8> = (0..hex.len())
.step_by(2)
.filter_map(|i| {
if i + 2 <= hex.len() {
u8::from_str_radix(&hex[i..i + 2], 16).ok()
} else {
None
}
})
.collect();
debug!("Converted {} hex chars to {} bytes", hex.len(), bytes.len());
bytes
}
}
#[derive(Debug, Serialize, Deserialize)]
struct LegacyZoneFile {
owner: String,
general: String,
twitter: String,
url: String,
nostr: String,
lightning: String,
btc: String,
subdomains: HashMap<String, SubDomain>,
}
#[cfg(feature = "wasm")]
pub mod wasm {
use super::*;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn parse_zonefile_from_clarity_buffer_hex_string(hex_string: &str) -> Result<Vec<u8>, JsValue> {
ZoneFile::from_clarity_buffer_hex_string(hex_string)
.and_then(|zf| zf.to_cbor().map_err(|e| serde_json::Error::io(std::io::Error::new(
std::io::ErrorKind::Other,
e.to_string()
))))
.map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "generate_zonefile")]
pub fn generate_zonefile(val: JsValue, compress: bool) -> Result<Vec<u8>, JsValue> {
let zonefile: ZoneFile = serde_wasm_bindgen::from_value(val)
.map_err(|e| JsValue::from_str(&format!("Failed to parse zonefile: {}", e)))?;
if compress {
zonefile.to_compressed_cbor()
.map_err(|e| JsValue::from_str(&format!("Failed to compress zonefile: {}", e)))
} else {
zonefile.to_cbor()
.map_err(|e| JsValue::from_str(&format!("Failed to generate CBOR: {}", e)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wallet::WalletType;
const SAMPLE_V0_ZONEFILE: &str = r#"{"owner":"SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8","general":"Pato Gómez","twitter":"@setpato","url":"pato.locker","nostr":"","lightning":"","btc":"bc1q4x98c6gwjj34lqs97zdvfyv2j0fcjhhw7hj0pc","subdomains":{"test":{"owner":"SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8","general":"test","twitter":"setpato","url":"testurl","nostr":"","lightning":"","btc":"bc1q4x98c6gwjj34lqs97zdvfyv2j0fcjhhw7hj0pc"}}}"#;
const SAMPLE_V0_ZONEFILE_EMPTY_SUBDOMAINS: &str = r#"{"owner":"SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8","general":"Pato Gómez","twitter":"@setpato","url":"pato.locker","nostr":"","lightning":"","btc":"bc1q4x98c6gwjj34lqs97zdvfyv2j0fcjhhw7hj0pc","subdomains":{}}"#;
const SAMPLE_ZONEFILE_HEX_STRING_LARRY_ID: &str = "7b226f776e6572223a2253503252374358454244474248374d374b3052484347514750593659364b524a314d37444541414d57222c2267656e6572616c223a22222c2274776974746572223a226c6172727973616c69627261222c2275726c223a22222c226e6f737472223a22222c226c696768746e696e67223a22222c22627463223a22222c22737562646f6d61696e73223a7b7d7d";
const SAMPLE_ZONEFILE_HEX_STRING_DEFAULT_ID: &str = "0x070a02000000148b0780a2617661316764656661756c7462696403";
fn setup() {
let _ = env_logger::builder()
.is_test(true)
.try_init();
}
#[test]
fn test_parse_zonefile_valid_v0() {
setup();
let result = ZoneFile::from_str(SAMPLE_V0_ZONEFILE);
assert!(result.is_ok());
let zonefile = result.unwrap();
match zonefile {
ZoneFile::V0 { owner, general, twitter, .. } => {
assert_eq!(&owner, "SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8");
assert_eq!(&general, "Pato Gómez");
assert_eq!(&twitter, "@setpato");
},
_ => panic!("Expected V0 zonefile"),
}
}
#[test]
#[ignore] fn test_parse_zonefile_valid_v1() {
setup();
debug!("Testing V1 zonefile parsing with hex string: {}", SAMPLE_ZONEFILE_HEX_STRING_DEFAULT_ID);
let result = ZoneFile::from_clarity_buffer_hex_string(SAMPLE_ZONEFILE_HEX_STRING_DEFAULT_ID);
if let Err(ref e) = result {
error!("Error parsing zonefile: {}", e);
}
assert!(result.is_ok());
let zonefile = result.unwrap();
debug!("Parsed zonefile: {:?}", zonefile);
match zonefile {
ZoneFile::V1 { default, .. } => {
assert_eq!(default, Some("id".to_string()));
},
_ => panic!("Expected V1 zonefile"),
}
}
#[test]
fn test_parse_zonefile_invalid() {
setup();
let result = ZoneFile::from_str("{invalid json}");
assert!(result.is_err());
}
#[test]
fn test_zonefile_roundtrip() {
setup();
let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE).unwrap();
let json = zonefile.to_string().unwrap();
let parsed_again = ZoneFile::from_str(&json).unwrap();
match (&zonefile, &parsed_again) {
(ZoneFile::V0 { owner: orig_owner, general: orig_general, subdomains: orig_subdomains, .. },
ZoneFile::V0 { owner: parsed_owner, general: parsed_general, subdomains: parsed_subdomains, .. }) => {
assert_eq!(orig_owner, parsed_owner);
assert_eq!(orig_general, parsed_general);
let orig_subdomain = orig_subdomains.get("test").unwrap();
let parsed_subdomain = parsed_subdomains.get("test").unwrap();
assert_eq!(orig_subdomain.general, parsed_subdomain.general);
assert_eq!(orig_subdomain.twitter, parsed_subdomain.twitter);
},
_ => panic!("Expected V0 zonefile"),
}
}
#[test]
fn test_cbor_roundtrip() {
setup();
let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE).unwrap();
let cbor_bytes = zonefile.to_cbor().unwrap();
let parsed_from_cbor = ZoneFile::from_bytes(&cbor_bytes).unwrap();
match (&zonefile, &parsed_from_cbor) {
(ZoneFile::V0 { owner: orig_owner, .. }, ZoneFile::V0 { owner: parsed_owner, .. }) => {
assert_eq!(orig_owner, parsed_owner);
},
_ => panic!("Expected V0 zonefile"),
}
let compressed_bytes = zonefile.to_compressed_cbor().unwrap();
let parsed_from_compressed = ZoneFile::from_bytes(&compressed_bytes).unwrap();
match (&zonefile, &parsed_from_compressed) {
(ZoneFile::V0 { owner: orig_owner, .. }, ZoneFile::V0 { owner: parsed_owner, .. }) => {
assert_eq!(orig_owner, parsed_owner);
},
_ => panic!("Expected V0 zonefile"),
}
assert!(compressed_bytes.len() < cbor_bytes.len());
debug!("CBOR size: {}, Compressed size: {}", cbor_bytes.len(), compressed_bytes.len());
}
#[test]
fn test_cbor_invalid_input() {
setup();
assert!(ZoneFile::from_cbor(&[]).is_err());
assert!(ZoneFile::from_cbor(&[0xFF, 0xFF, 0xFF]).is_err());
}
#[test]
fn test_cbor_size() {
setup();
let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE).unwrap();
let json_size = SAMPLE_V0_ZONEFILE.len();
let cbor_size = zonefile.to_cbor().unwrap().len();
assert!(cbor_size < json_size);
debug!("JSON size: {}, CBOR size: {}", json_size, cbor_size);
}
#[test]
fn test_cbor_empty_fields() {
setup();
let zonefile = ZoneFile::from_str(SAMPLE_V0_ZONEFILE_EMPTY_SUBDOMAINS).unwrap();
let cbor_bytes = zonefile.to_cbor().unwrap();
let parsed_zonefile = ZoneFile::from_cbor(&cbor_bytes).unwrap();
match parsed_zonefile {
ZoneFile::V0 { nostr, lightning, subdomains, .. } => {
assert_eq!(nostr, "");
assert_eq!(lightning, "");
assert!(subdomains.is_empty());
},
_ => panic!("Expected V0 zonefile"),
}
}
#[test]
fn test_clarity_buffer_conversion() {
setup();
let hex = "0x5361746f736869"; let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
assert_eq!(bytes, b"Satoshi");
let hex = "5361746f736869"; let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
assert_eq!(bytes, b"Satoshi");
let hex = "";
let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
assert_eq!(bytes, Vec::<u8>::new());
let hex = "5361746f73686"; let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
assert_eq!(bytes, vec![0x53, 0x61, 0x74, 0x6f, 0x73, 0x68]);
let hex = "0xZZ";
let bytes = ZoneFile::clarity_buffer_to_uint8_array(hex);
assert_eq!(bytes, Vec::<u8>::new()); }
#[test]
fn test_from_clarity_buffer_hex_string() {
setup();
let hex = format!("0x{}", hex::encode(SAMPLE_V0_ZONEFILE));
debug!("Testing with hex string: {}", hex);
let result = ZoneFile::from_clarity_buffer_hex_string(&hex);
assert!(result.is_ok());
let zonefile = result.unwrap();
match zonefile {
ZoneFile::V0 { owner, general, .. } => {
assert_eq!(&owner, "SP3D03X5BHMNSAAW71NN7BQRMV4DW2G4JB3MZAGJ8");
assert_eq!(&general, "Pato Gómez");
},
_ => panic!("Expected V0 zonefile"),
}
let result = ZoneFile::from_clarity_buffer_hex_string("0xZZ");
assert!(result.is_err());
let result = ZoneFile::from_clarity_buffer_hex_string("");
assert!(result.is_err());
}
#[test]
fn test_parse_hex_string_sample() {
setup();
let result = ZoneFile::from_clarity_buffer_hex_string(SAMPLE_ZONEFILE_HEX_STRING_LARRY_ID);
assert!(result.is_ok());
let zonefile = result.unwrap();
match zonefile {
ZoneFile::V0 { owner, general, twitter, url, nostr, lightning, btc, subdomains } => {
assert_eq!(owner, "SP2R7CXEBDGBH7M7K0RHCGQGPY6Y6KRJ1M7DEAAMW");
assert_eq!(general, "");
assert_eq!(twitter, "larrysalibra");
assert_eq!(url, "");
assert_eq!(nostr, "");
assert_eq!(lightning, "");
assert_eq!(btc, "");
assert!(subdomains.is_empty());
},
_ => panic!("Expected V0 zonefile"),
}
}
#[test]
fn test_version_handling() {
setup();
let json = r#"{"owner":"SP123","general":"Test","twitter":"@test","url":"test.url","nostr":"","lightning":"","btc":"","subdomains":{}}"#;
let zonefile = ZoneFile::from_str(json).unwrap();
let serialized = zonefile.to_string().unwrap();
debug!("Serialized V0 from legacy: {}", serialized);
assert!(serialized.contains(r#""v":"0""#), "Serialized output should contain version tag");
let json_v0 = r#"{"v":"0","owner":"SP123","general":"Test","twitter":"@test","url":"test.url","nostr":"","lightning":"","btc":"","subdomains":{}}"#;
let zonefile = ZoneFile::from_str(json_v0).unwrap();
let serialized = zonefile.to_string().unwrap();
debug!("Serialized V0 from explicit: {}", serialized);
assert!(serialized.contains(r#""v":"0""#), "Serialized output should contain version tag");
let cbor_bytes = zonefile.to_cbor().unwrap();
let parsed_from_cbor = ZoneFile::from_cbor(&cbor_bytes).unwrap();
let serialized_from_cbor = parsed_from_cbor.to_string().unwrap();
debug!("Serialized from CBOR: {}", serialized_from_cbor);
assert!(serialized_from_cbor.contains(r#""v":"0""#), "CBOR roundtrip should preserve version tag");
let json_v1 = r#"{"v":"1","default":"test.btc"}"#;
let zonefile = ZoneFile::from_str(json_v1).unwrap();
match zonefile {
ZoneFile::V1 { default, id: _, resources: _, wallets: _, dns: _ } => assert_eq!(default, Some("test.btc".to_string())),
_ => panic!("Expected V1 zonefile"),
}
let v1 = ZoneFile::new_v1("test.btc");
let serialized = v1.to_string().unwrap();
debug!("Serialized V1: {}", serialized);
assert!(serialized.contains(r#""v":"1""#), "Serialized V1 output should contain version tag");
let v0 = ZoneFile::new_v0(
"SP123".to_string(),
"Test".to_string(),
"@test".to_string(),
"test.url".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
HashMap::new(),
);
let serialized = v0.to_string().unwrap();
debug!("Serialized V0 from new: {}", serialized);
assert!(serialized.contains(r#""v":"0""#), "Serialized V0 output should contain version tag");
}
#[test]
fn test_version_tag_simple() {
setup();
let v0 = ZoneFile::new_v0(
"SP123".to_string(),
"Test".to_string(),
"@test".to_string(),
"test.url".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
HashMap::new(),
);
let serialized = serde_json::to_string(&v0).unwrap();
debug!("Direct V0 serialization: {}", serialized);
assert!(serialized.contains(r#""v":"0""#), "Direct V0 serialization should contain version tag");
let v1 = ZoneFile::new_v1("test.btc");
let serialized = serde_json::to_string(&v1).unwrap();
debug!("Direct V1 serialization: {}", serialized);
assert!(serialized.contains(r#""v":"1""#), "Direct V1 serialization should contain version tag");
}
#[test]
fn test_zonefile_v1_with_assets() {
setup();
let mut resources = HashMap::new();
resources.insert("greeting.txt".to_string(), Resource {
val: Some(ResourceValue::Text("Hello, world!".to_string())),
mime: Some("text/plain".to_string()),
});
resources.insert("data.bin".to_string(), Resource {
val: Some(ResourceValue::Binary(vec![0x01, 0x02, 0x03])),
mime: Some("application/octet-stream".to_string()),
});
let zonefile = ZoneFile::V1 {
default: Some("example.btc".to_string()),
id: None,
resources: Some(resources),
wallets: None,
dns: None,
};
let cbor_bytes = zonefile.to_cbor().unwrap();
let parsed_zonefile = ZoneFile::from_cbor(&cbor_bytes).unwrap();
match parsed_zonefile {
ZoneFile::V1 { resources: Some(parsed_resources), .. } => {
assert_eq!(parsed_resources.len(), 2);
assert!(parsed_resources.contains_key("greeting.txt"));
assert!(parsed_resources.contains_key("data.bin"));
},
_ => panic!("Expected V1 zonefile with resources"),
}
}
#[test]
fn test_v1_zonefile_with_wallets() {
setup();
let mut wallets = HashMap::new();
wallets.insert(WalletType::bitcoin(), "bc1qxxx...".to_string());
wallets.insert(WalletType::stacks(), "SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B".to_string());
wallets.insert(WalletType::solana(), "DxPv2QMA5cWR5Xj7BHt8J9LZkxGGx8DDsicafbLnGm9R".to_string());
wallets.insert(WalletType::ethereum(), "0x123...".to_string());
wallets.insert(WalletType::lightning(), "larry@getalby.com".to_string());
let avatar_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; let banner_data = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
let mut resources = HashMap::new();
resources.insert("avatar".to_string(), Resource {
val: Some(ResourceValue::Binary(avatar_data.clone())),
mime: Some("image/png".to_string()),
});
resources.insert("banner".to_string(), Resource {
val: Some(ResourceValue::Binary(banner_data.clone())),
mime: Some("image/jpeg".to_string()),
});
let zonefile = ZoneFile::V1 {
default: Some("larry.btc".to_string()),
id: None,
resources: Some(resources),
wallets: Some(wallets),
dns: None,
};
let json = serde_json::to_string_pretty(&zonefile).unwrap();
debug!("Complete V1 zonefile JSON:\n{}", json);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["v"], "1");
assert_eq!(parsed["default"], "larry.btc");
assert_eq!(parsed["wallets"]["0"], "bc1qxxx..."); assert_eq!(parsed["wallets"]["5757"], "SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B"); assert_eq!(parsed["wallets"]["501"], "DxPv2QMA5cWR5Xj7BHt8J9LZkxGGx8DDsicafbLnGm9R"); assert_eq!(parsed["wallets"]["60"], "0x123..."); assert_eq!(parsed["wallets"]["lightning"], "larry@getalby.com");
assert_eq!(parsed["resources"]["avatar"]["mime"], "image/png");
assert_eq!(parsed["resources"]["banner"]["mime"], "image/jpeg");
let deserialized: ZoneFile = serde_json::from_str(&json).unwrap();
match deserialized {
ZoneFile::V1 { default, id, resources, wallets, dns } => {
assert_eq!(default, Some("larry.btc".to_string()));
assert_eq!(id, None);
assert_eq!(dns, None);
let wallets = wallets.unwrap();
assert_eq!(wallets.get(&WalletType::bitcoin()).unwrap(), "bc1qxxx...");
assert_eq!(wallets.get(&WalletType::stacks()).unwrap(), "SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B");
assert_eq!(wallets.get(&WalletType::solana()).unwrap(), "DxPv2QMA5cWR5Xj7BHt8J9LZkxGGx8DDsicafbLnGm9R");
assert_eq!(wallets.get(&WalletType::ethereum()).unwrap(), "0x123...");
assert_eq!(wallets.get(&WalletType::lightning()).unwrap(), "larry@getalby.com");
let resources = resources.unwrap();
match &resources.get("avatar").unwrap().val {
Some(ResourceValue::Binary(data)) => assert_eq!(data, &avatar_data),
_ => panic!("Expected Binary resource value for avatar"),
}
assert_eq!(resources.get("avatar").unwrap().mime, Some("image/png".to_string()));
match &resources.get("banner").unwrap().val {
Some(ResourceValue::Binary(data)) => assert_eq!(data, &banner_data),
_ => panic!("Expected Binary resource value for banner"),
}
assert_eq!(resources.get("banner").unwrap().mime, Some("image/jpeg".to_string()));
},
_ => panic!("Expected V1 zonefile"),
}
}
#[test]
fn test_zonefile_v1_with_dns() {
setup();
let zonefile = ZoneFile::V1 {
default: Some("example.btc".to_string()),
id: None,
resources: None,
wallets: None,
dns: Some(Dns {
zone: Some("example.com".to_string()),
}),
};
let json = serde_json::to_string_pretty(&zonefile).unwrap();
debug!("V1 zonefile with DNS JSON:\n{}", json);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["v"], "1");
assert_eq!(parsed["default"], "example.btc");
assert_eq!(parsed["dns"], "example.com");
let zonefile = ZoneFile::V1 {
default: Some("example.btc".to_string()),
id: None,
resources: None,
wallets: None,
dns: Some(Dns { zone: None }),
};
let json = serde_json::to_string_pretty(&zonefile).unwrap();
debug!("V1 zonefile with empty DNS JSON:\n{}", json);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(!parsed.as_object().unwrap().contains_key("dns"));
let zonefile = ZoneFile::V1 {
default: Some("example.btc".to_string()),
id: None,
resources: None,
wallets: None,
dns: None,
};
let json = serde_json::to_string_pretty(&zonefile).unwrap();
debug!("V1 zonefile without DNS JSON:\n{}", json);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(!parsed.as_object().unwrap().contains_key("dns"));
let zonefile = ZoneFile::V1 {
default: Some("example.btc".to_string()),
id: None,
resources: None,
wallets: None,
dns: Some(Dns {
zone: Some("example.com".to_string()),
}),
};
let cbor_bytes = zonefile.to_cbor().unwrap();
let parsed_zonefile = ZoneFile::from_cbor(&cbor_bytes).unwrap();
match parsed_zonefile {
ZoneFile::V1 { dns: Some(dns), .. } => {
assert_eq!(dns.zone, Some("example.com".to_string()));
},
_ => panic!("Expected V1 zonefile with DNS"),
}
}
}