use crate::error::{Error, Result};
use crate::wallet::types::{Outpoint, Protocol, SecurityLevel, TxId, MAX_SATOSHIS};
pub fn validate_satoshis(value: u64, name: &str, min: Option<u64>) -> Result<u64> {
if value > MAX_SATOSHIS {
return Err(Error::WalletError(format!(
"Invalid {}: {} exceeds maximum of {} satoshis",
name, value, MAX_SATOSHIS
)));
}
if let Some(min_val) = min {
if value < min_val {
return Err(Error::WalletError(format!(
"Invalid {}: must be at least {} satoshis",
name, min_val
)));
}
}
Ok(value)
}
pub fn validate_integer(
value: Option<i64>,
name: &str,
default: Option<i64>,
min: Option<i64>,
max: Option<i64>,
) -> Result<i64> {
let value = match value {
Some(v) => v,
None => default.ok_or_else(|| {
Error::WalletError(format!("Invalid {}: a valid integer is required", name))
})?,
};
if let Some(min_val) = min {
if value < min_val {
return Err(Error::WalletError(format!(
"Invalid {}: must be at least {}",
name, min_val
)));
}
}
if let Some(max_val) = max {
if value > max_val {
return Err(Error::WalletError(format!(
"Invalid {}: must be no more than {}",
name, max_val
)));
}
}
Ok(value)
}
pub fn validate_integer_u32(
value: Option<u32>,
name: &str,
default: Option<u32>,
min: Option<u32>,
max: Option<u32>,
) -> Result<u32> {
let value = match value {
Some(v) => v,
None => default.ok_or_else(|| {
Error::WalletError(format!("Invalid {}: a valid integer is required", name))
})?,
};
if let Some(min_val) = min {
if value < min_val {
return Err(Error::WalletError(format!(
"Invalid {}: must be at least {}",
name, min_val
)));
}
}
if let Some(max_val) = max {
if value > max_val {
return Err(Error::WalletError(format!(
"Invalid {}: must be no more than {}",
name, max_val
)));
}
}
Ok(value)
}
pub fn validate_positive_integer_or_zero(value: u64, _name: &str) -> Result<u64> {
Ok(value)
}
pub fn validate_string_length<'a>(
s: &'a str,
name: &str,
min: Option<usize>,
max: Option<usize>,
) -> Result<&'a str> {
let bytes = s.len();
if let Some(min_len) = min {
if bytes < min_len {
return Err(Error::WalletError(format!(
"Invalid {}: must be at least {} bytes, got {}",
name, min_len, bytes
)));
}
}
if let Some(max_len) = max {
if bytes > max_len {
return Err(Error::WalletError(format!(
"Invalid {}: must be no more than {} bytes, got {}",
name, max_len, bytes
)));
}
}
Ok(s)
}
pub fn validate_optional_string_length(
s: Option<&str>,
name: &str,
min: Option<usize>,
max: Option<usize>,
) -> Result<Option<String>> {
match s {
Some(s) => Ok(Some(validate_string_length(s, name, min, max)?.to_string())),
None => Ok(None),
}
}
#[allow(unknown_lints, clippy::manual_is_multiple_of)]
pub fn validate_hex_string(
s: &str,
name: &str,
min_chars: Option<usize>,
max_chars: Option<usize>,
) -> Result<String> {
let s = s.trim().to_lowercase();
if s.len() % 2 != 0 {
return Err(Error::WalletError(format!(
"Invalid {}: hex string must have even length, got {}",
name,
s.len()
)));
}
if !s.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(Error::WalletError(format!(
"Invalid {}: must be a valid hexadecimal string",
name
)));
}
if let Some(min_len) = min_chars {
if s.len() < min_len {
return Err(Error::WalletError(format!(
"Invalid {}: must be at least {} characters, got {}",
name,
min_len,
s.len()
)));
}
}
if let Some(max_len) = max_chars {
if s.len() > max_len {
return Err(Error::WalletError(format!(
"Invalid {}: must be no more than {} characters, got {}",
name,
max_len,
s.len()
)));
}
}
Ok(s)
}
pub fn validate_optional_hex_string(
s: Option<&str>,
name: &str,
min_chars: Option<usize>,
max_chars: Option<usize>,
) -> Result<Option<String>> {
match s {
Some(s) => Ok(Some(validate_hex_string(s, name, min_chars, max_chars)?)),
None => Ok(None),
}
}
#[allow(unknown_lints, clippy::manual_is_multiple_of)]
pub fn is_hex_string(s: &str) -> bool {
let s = s.trim();
if s.len() % 2 != 0 {
return false;
}
s.chars().all(|c| c.is_ascii_hexdigit())
}
pub fn validate_base64_string(
s: &str,
name: &str,
min_decoded_bytes: Option<usize>,
max_decoded_bytes: Option<usize>,
) -> Result<String> {
let s = s.trim();
if s.is_empty() {
return Err(Error::WalletError(format!(
"Invalid {}: must be a valid base64 string",
name
)));
}
let mut padding_count = 0;
for (i, c) in s.chars().enumerate() {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' => continue,
'=' => {
if i < s.len() - 2 {
return Err(Error::WalletError(format!(
"Invalid {}: padding must be at the end",
name
)));
}
padding_count += 1;
}
_ => {
return Err(Error::WalletError(format!(
"Invalid {}: must be a valid base64 string",
name
)));
}
}
}
if padding_count > 2 {
return Err(Error::WalletError(format!(
"Invalid {}: too much padding",
name
)));
}
let encoded_len = s.len() - padding_count;
let decoded_bytes = (encoded_len * 3) / 4;
if let Some(min_len) = min_decoded_bytes {
if decoded_bytes < min_len {
return Err(Error::WalletError(format!(
"Invalid {}: decoded content must be at least {} bytes",
name, min_len
)));
}
}
if let Some(max_len) = max_decoded_bytes {
if decoded_bytes > max_len {
return Err(Error::WalletError(format!(
"Invalid {}: decoded content must be no more than {} bytes",
name, max_len
)));
}
}
Ok(s.to_string())
}
pub fn validate_optional_base64_string(
s: Option<&str>,
name: &str,
min_decoded_bytes: Option<usize>,
max_decoded_bytes: Option<usize>,
) -> Result<Option<String>> {
match s {
Some(s) => Ok(Some(validate_base64_string(
s,
name,
min_decoded_bytes,
max_decoded_bytes,
)?)),
None => Ok(None),
}
}
pub fn parse_wallet_outpoint(outpoint: &str) -> Result<Outpoint> {
let parts: Vec<&str> = outpoint.split('.').collect();
if parts.len() != 2 {
return Err(Error::WalletError(format!(
"Invalid outpoint: expected format 'txid.vout', got '{}'",
outpoint
)));
}
let txid = validate_hex_string(parts[0], "outpoint txid", Some(64), Some(64))?;
let vout: u32 = parts[1].parse().map_err(|_| {
Error::WalletError(format!(
"Invalid outpoint vout: '{}' is not a valid integer",
parts[1]
))
})?;
let txid_bytes = crate::primitives::from_hex(&txid)
.map_err(|_| Error::WalletError("Invalid txid hex".to_string()))?;
let mut txid_array: TxId = [0u8; 32];
txid_array.copy_from_slice(&txid_bytes);
Ok(Outpoint::new(txid_array, vout))
}
pub fn validate_outpoint_string(outpoint: &str, _name: &str) -> Result<String> {
let parsed = parse_wallet_outpoint(outpoint)?;
Ok(parsed.to_string())
}
pub fn validate_optional_outpoint_string(
outpoint: Option<&str>,
name: &str,
) -> Result<Option<String>> {
match outpoint {
Some(o) => Ok(Some(validate_outpoint_string(o, name)?)),
None => Ok(None),
}
}
fn validate_identifier(
s: &str,
name: &str,
min: Option<usize>,
max: Option<usize>,
) -> Result<String> {
let s = s.trim().to_lowercase();
let bytes = s.len();
if let Some(min_len) = min {
if bytes < min_len {
return Err(Error::WalletError(format!(
"Invalid {}: must be at least {} bytes",
name, min_len
)));
}
}
if let Some(max_len) = max {
if bytes > max_len {
return Err(Error::WalletError(format!(
"Invalid {}: must be no more than {} bytes",
name, max_len
)));
}
}
Ok(s)
}
pub fn validate_basket(s: &str) -> Result<String> {
validate_identifier(s, "basket", Some(1), Some(300))
}
pub fn validate_optional_basket(s: Option<&str>) -> Result<Option<String>> {
match s {
Some(s) => Ok(Some(validate_basket(s)?)),
None => Ok(None),
}
}
pub fn validate_label(s: &str) -> Result<String> {
validate_identifier(s, "label", Some(1), Some(300))
}
pub fn validate_tag(s: &str) -> Result<String> {
validate_identifier(s, "tag", Some(1), Some(300))
}
pub fn validate_originator(s: Option<&str>) -> Result<Option<String>> {
let s = match s {
Some(s) => s,
None => return Ok(None),
};
let s = s.trim().to_lowercase();
validate_string_length(&s, "originator", Some(1), Some(250))?;
for part in s.split('.') {
validate_string_length(part, "originator part", Some(1), Some(63))?;
}
Ok(Some(s))
}
pub fn validate_description_5_2000(desc: &str, name: &str) -> Result<String> {
validate_string_length(desc, name, Some(5), Some(2000))?;
Ok(desc.to_string())
}
pub fn validate_description_5_50(desc: &str, name: &str) -> Result<String> {
validate_string_length(desc, name, Some(5), Some(50))?;
Ok(desc.to_string())
}
#[derive(Debug, Clone)]
pub struct ValidCreateActionInput {
pub outpoint: Outpoint,
pub input_description: String,
pub sequence_number: u32,
pub unlocking_script: Option<Vec<u8>>,
pub unlocking_script_length: u32,
}
#[derive(Debug, Clone)]
pub struct ValidCreateActionOutput {
pub locking_script: Vec<u8>,
pub satoshis: u64,
pub output_description: String,
pub basket: Option<String>,
pub custom_instructions: Option<String>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ValidCreateActionOptions {
pub sign_and_process: bool,
pub accept_delayed_broadcast: bool,
pub known_txids: Vec<TxId>,
pub return_txid_only: bool,
pub no_send: bool,
pub no_send_change: Vec<Outpoint>,
pub send_with: Vec<TxId>,
pub randomize_outputs: bool,
}
impl Default for ValidCreateActionOptions {
fn default() -> Self {
Self {
sign_and_process: true,
accept_delayed_broadcast: true,
known_txids: Vec::new(),
return_txid_only: false,
no_send: false,
no_send_change: Vec::new(),
send_with: Vec::new(),
randomize_outputs: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ValidCreateActionArgs {
pub description: String,
pub input_beef: Option<Vec<u8>>,
pub inputs: Vec<ValidCreateActionInput>,
pub outputs: Vec<ValidCreateActionOutput>,
pub lock_time: u32,
pub version: u32,
pub labels: Vec<String>,
pub options: ValidCreateActionOptions,
pub is_send_with: bool,
pub is_delayed: bool,
pub is_no_send: bool,
pub is_new_tx: bool,
pub is_remix_change: bool,
pub is_sign_action: bool,
}
#[derive(Debug, Clone)]
pub struct CreateActionInputRaw {
pub outpoint: String,
pub input_description: String,
pub unlocking_script: Option<String>,
pub unlocking_script_length: Option<u32>,
pub sequence_number: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct CreateActionOutputRaw {
pub locking_script: String,
pub satoshis: u64,
pub output_description: String,
pub basket: Option<String>,
pub custom_instructions: Option<String>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct CreateActionOptionsRaw {
pub sign_and_process: Option<bool>,
pub accept_delayed_broadcast: Option<bool>,
pub known_txids: Vec<String>,
pub return_txid_only: Option<bool>,
pub no_send: Option<bool>,
pub no_send_change: Vec<String>,
pub send_with: Vec<String>,
pub randomize_outputs: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct CreateActionArgsRaw {
pub description: String,
pub input_beef: Option<Vec<u8>>,
pub inputs: Vec<CreateActionInputRaw>,
pub outputs: Vec<CreateActionOutputRaw>,
pub lock_time: Option<u32>,
pub version: Option<u32>,
pub labels: Vec<String>,
pub options: Option<CreateActionOptionsRaw>,
}
pub fn validate_create_action_input(
input: &CreateActionInputRaw,
) -> Result<ValidCreateActionInput> {
if input.unlocking_script.is_none() && input.unlocking_script_length.is_none() {
return Err(Error::WalletError(
"unlockingScript or unlockingScriptLength must be provided".to_string(),
));
}
let unlocking_script = input
.unlocking_script
.as_ref()
.map(|s| validate_hex_string(s, "unlockingScript", None, None))
.transpose()?
.map(|s| crate::primitives::from_hex(&s).unwrap());
let unlocking_script_length = input
.unlocking_script_length
.or_else(|| unlocking_script.as_ref().map(|s| s.len() as u32))
.unwrap_or(0);
if let Some(ref script) = unlocking_script {
if unlocking_script_length != script.len() as u32 {
return Err(Error::WalletError(
"unlockingScriptLength must match unlockingScript length if both provided"
.to_string(),
));
}
}
Ok(ValidCreateActionInput {
outpoint: parse_wallet_outpoint(&input.outpoint)?,
input_description: validate_description_5_2000(
&input.input_description,
"inputDescription",
)?,
sequence_number: input.sequence_number.unwrap_or(0xffffffff),
unlocking_script,
unlocking_script_length,
})
}
pub fn validate_create_action_output(
output: &CreateActionOutputRaw,
) -> Result<ValidCreateActionOutput> {
let locking_script = validate_hex_string(&output.locking_script, "lockingScript", None, None)?;
let locking_script_bytes = crate::primitives::from_hex(&locking_script)
.map_err(|_| Error::WalletError("Invalid lockingScript hex".to_string()))?;
Ok(ValidCreateActionOutput {
locking_script: locking_script_bytes,
satoshis: validate_satoshis(output.satoshis, "satoshis", None)?,
output_description: validate_description_5_2000(
&output.output_description,
"outputDescription",
)?,
basket: validate_optional_basket(output.basket.as_deref())?,
custom_instructions: output.custom_instructions.clone(),
tags: output
.tags
.iter()
.map(|t| validate_tag(t))
.collect::<Result<_>>()?,
})
}
pub fn validate_create_action_options(
options: Option<&CreateActionOptionsRaw>,
) -> Result<ValidCreateActionOptions> {
let options = match options {
Some(o) => o,
None => return Ok(ValidCreateActionOptions::default()),
};
Ok(ValidCreateActionOptions {
sign_and_process: options.sign_and_process.unwrap_or(true),
accept_delayed_broadcast: options.accept_delayed_broadcast.unwrap_or(true),
known_txids: options
.known_txids
.iter()
.map(|t| {
let hex = validate_hex_string(t, "knownTxid", Some(64), Some(64))?;
let bytes = crate::primitives::from_hex(&hex)
.map_err(|_| Error::WalletError("Invalid knownTxid hex".to_string()))?;
let mut arr: TxId = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
})
.collect::<Result<_>>()?,
return_txid_only: options.return_txid_only.unwrap_or(false),
no_send: options.no_send.unwrap_or(false),
no_send_change: options
.no_send_change
.iter()
.map(|o| parse_wallet_outpoint(o))
.collect::<Result<_>>()?,
send_with: options
.send_with
.iter()
.map(|t| {
let hex = validate_hex_string(t, "sendWith", Some(64), Some(64))?;
let bytes = crate::primitives::from_hex(&hex)
.map_err(|_| Error::WalletError("Invalid sendWith hex".to_string()))?;
let mut arr: TxId = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
})
.collect::<Result<_>>()?,
randomize_outputs: options.randomize_outputs.unwrap_or(true),
})
}
pub fn validate_create_action_args(args: &CreateActionArgsRaw) -> Result<ValidCreateActionArgs> {
let description = validate_description_5_2000(&args.description, "description")?;
let inputs: Vec<ValidCreateActionInput> = args
.inputs
.iter()
.map(validate_create_action_input)
.collect::<Result<_>>()?;
let outputs: Vec<ValidCreateActionOutput> = args
.outputs
.iter()
.map(validate_create_action_output)
.collect::<Result<_>>()?;
let labels: Vec<String> = args
.labels
.iter()
.map(|l| validate_label(l))
.collect::<Result<_>>()?;
let options = validate_create_action_options(args.options.as_ref())?;
let is_send_with = !options.send_with.is_empty();
let is_remix_change = !is_send_with && inputs.is_empty() && outputs.is_empty();
let is_new_tx = is_remix_change || !inputs.is_empty() || !outputs.is_empty();
let is_sign_action = is_new_tx
&& (!options.sign_and_process || inputs.iter().any(|i| i.unlocking_script.is_none()));
Ok(ValidCreateActionArgs {
description,
input_beef: args.input_beef.clone(),
inputs,
outputs,
lock_time: args.lock_time.unwrap_or(0),
version: args.version.unwrap_or(1),
labels,
options,
is_send_with,
is_delayed: true,
is_no_send: false,
is_new_tx,
is_remix_change,
is_sign_action,
})
}
#[derive(Debug, Clone)]
pub struct SignActionSpendRaw {
pub unlocking_script: String,
pub sequence_number: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct ValidSignActionSpend {
pub unlocking_script: Vec<u8>,
pub sequence_number: u32,
}
pub fn validate_sign_action_spend(spend: &SignActionSpendRaw) -> Result<ValidSignActionSpend> {
let hex = validate_hex_string(&spend.unlocking_script, "unlockingScript", None, None)?;
let bytes = crate::primitives::from_hex(&hex)
.map_err(|_| Error::WalletError("Invalid unlockingScript hex".to_string()))?;
Ok(ValidSignActionSpend {
unlocking_script: bytes,
sequence_number: spend.sequence_number.unwrap_or(0xffffffff),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryMode {
Any,
All,
}
pub fn validate_query_mode(mode: Option<&str>, name: &str) -> Result<QueryMode> {
match mode {
None | Some("any") => Ok(QueryMode::Any),
Some("all") => Ok(QueryMode::All),
Some(other) => Err(Error::WalletError(format!(
"Invalid {}: must be 'any' or 'all', got '{}'",
name, other
))),
}
}
#[derive(Debug, Clone)]
pub struct ValidListOutputsArgs {
pub basket: String,
pub tags: Vec<String>,
pub tag_query_mode: QueryMode,
pub include_locking_scripts: bool,
pub include_transactions: bool,
pub include_custom_instructions: bool,
pub include_tags: bool,
pub include_labels: bool,
pub limit: u32,
pub offset: i32,
pub seek_permission: bool,
pub known_txids: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ValidListActionsArgs {
pub labels: Vec<String>,
pub label_query_mode: QueryMode,
pub include_labels: bool,
pub include_inputs: bool,
pub include_input_source_locking_scripts: bool,
pub include_input_unlocking_scripts: bool,
pub include_outputs: bool,
pub include_output_locking_scripts: bool,
pub limit: u32,
pub offset: u32,
pub seek_permission: bool,
}
pub fn validate_certificate_fields(
fields: &std::collections::HashMap<String, String>,
) -> Result<std::collections::HashMap<String, String>> {
let mut result = std::collections::HashMap::new();
for (field_name, value) in fields {
validate_string_length(field_name, "field name", Some(1), Some(50))?;
result.insert(field_name.clone(), value.clone());
}
Ok(result)
}
pub fn validate_keyring_revealer(kr: &str, name: &str) -> Result<String> {
if kr == "certifier" {
return Ok(kr.to_string());
}
validate_hex_string(kr, name, Some(66), Some(66))
}
pub fn validate_protocol_tuple(tuple: (u8, &str)) -> Result<Protocol> {
let security_level = SecurityLevel::from_u8(tuple.0).ok_or_else(|| {
Error::WalletError(format!(
"Invalid security level: {} (must be 0, 1, or 2)",
tuple.0
))
})?;
let protocol_name = crate::wallet::types::validate_protocol_name(tuple.1)?;
Ok(Protocol::new(security_level, protocol_name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_satoshis() {
assert!(validate_satoshis(0, "test", None).is_ok());
assert!(validate_satoshis(MAX_SATOSHIS, "test", None).is_ok());
assert!(validate_satoshis(MAX_SATOSHIS + 1, "test", None).is_err());
assert!(validate_satoshis(100, "test", Some(50)).is_ok());
assert!(validate_satoshis(100, "test", Some(200)).is_err());
}
#[test]
fn test_validate_integer() {
assert_eq!(
validate_integer(Some(5), "test", None, None, None).unwrap(),
5
);
assert_eq!(
validate_integer(None, "test", Some(10), None, None).unwrap(),
10
);
assert!(validate_integer(None, "test", None, None, None).is_err());
assert!(validate_integer(Some(5), "test", None, Some(10), None).is_err());
assert!(validate_integer(Some(15), "test", None, None, Some(10)).is_err());
}
#[test]
fn test_validate_string_length() {
assert!(validate_string_length("hello", "test", None, None).is_ok());
assert!(validate_string_length("hi", "test", Some(5), None).is_err());
assert!(validate_string_length("hello world", "test", None, Some(5)).is_err());
}
#[test]
fn test_validate_hex_string() {
assert!(validate_hex_string("deadbeef", "test", None, None).is_ok());
assert!(validate_hex_string("DEADBEEF", "test", None, None).is_ok()); assert!(validate_hex_string(" deadbeef ", "test", None, None).is_ok()); assert!(validate_hex_string("deadbee", "test", None, None).is_err()); assert!(validate_hex_string("ghijkl", "test", None, None).is_err()); assert!(validate_hex_string("dead", "test", Some(8), None).is_err()); assert!(validate_hex_string("deadbeef", "test", None, Some(4)).is_err());
}
#[test]
fn test_validate_base64_string() {
assert!(validate_base64_string("SGVsbG8=", "test", None, None).is_ok());
assert!(validate_base64_string("SGVsbG8", "test", None, None).is_ok()); assert!(validate_base64_string("", "test", None, None).is_err()); assert!(validate_base64_string("SGVsb@8=", "test", None, None).is_err());
}
#[test]
fn test_parse_wallet_outpoint() {
let txid = "0000000000000000000000000000000000000000000000000000000000000001";
let outpoint_str = format!("{}.5", txid);
let outpoint = parse_wallet_outpoint(&outpoint_str).unwrap();
assert_eq!(outpoint.vout, 5);
assert_eq!(outpoint.txid[31], 1);
}
#[test]
fn test_parse_wallet_outpoint_invalid() {
assert!(parse_wallet_outpoint("invalid").is_err());
assert!(parse_wallet_outpoint("abc.1").is_err()); assert!(parse_wallet_outpoint("00000000000000000000000000000000.abc").is_err());
}
#[test]
fn test_validate_basket() {
assert!(validate_basket("my-basket").is_ok());
assert!(validate_basket(" MY_BASKET ").is_ok()); assert!(validate_basket("").is_err()); assert!(validate_basket(&"a".repeat(301)).is_err()); }
#[test]
fn test_validate_label() {
assert!(validate_label("my-label").is_ok());
assert!(validate_label("").is_err());
}
#[test]
fn test_validate_tag() {
assert!(validate_tag("my-tag").is_ok());
assert!(validate_tag("").is_err());
}
#[test]
fn test_validate_originator() {
assert!(validate_originator(None).unwrap().is_none());
assert!(validate_originator(Some("example.com")).is_ok());
assert!(validate_originator(Some("sub.example.com")).is_ok());
assert!(validate_originator(Some("")).is_err()); assert!(validate_originator(Some(&"a".repeat(251))).is_err()); }
#[test]
fn test_validate_query_mode() {
assert_eq!(validate_query_mode(None, "test").unwrap(), QueryMode::Any);
assert_eq!(
validate_query_mode(Some("any"), "test").unwrap(),
QueryMode::Any
);
assert_eq!(
validate_query_mode(Some("all"), "test").unwrap(),
QueryMode::All
);
assert!(validate_query_mode(Some("invalid"), "test").is_err());
}
#[test]
fn test_validate_protocol_tuple() {
let proto = validate_protocol_tuple((1, "test application")).unwrap();
assert_eq!(proto.security_level, SecurityLevel::App);
assert_eq!(proto.protocol_name, "test application");
assert!(validate_protocol_tuple((5, "test")).is_err());
assert!(validate_protocol_tuple((1, "abc")).is_err());
}
#[test]
fn test_is_hex_string() {
assert!(is_hex_string("deadbeef"));
assert!(is_hex_string("DEADBEEF"));
assert!(!is_hex_string("deadbee")); assert!(!is_hex_string("ghijkl")); }
}