use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Protocol {
#[serde(rename = "SHIP")]
Ship,
#[serde(rename = "SLAP")]
Slap,
}
impl Protocol {
pub fn as_str(&self) -> &'static str {
match self {
Self::Ship => "SHIP",
Self::Slap => "SLAP",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"SHIP" => Some(Self::Ship),
"SLAP" => Some(Self::Slap),
_ => None,
}
}
}
impl fmt::Display for Protocol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum NetworkPreset {
#[default]
Mainnet,
Testnet,
Local,
}
impl NetworkPreset {
pub fn slap_trackers(&self) -> Vec<&'static str> {
match self {
Self::Mainnet => vec![
"https://overlay-us-1.bsvb.tech",
"https://overlay-eu-1.bsvb.tech",
"https://overlay-ap-1.bsvb.tech",
"https://users.bapp.dev",
],
Self::Testnet => vec!["https://testnet-users.bapp.dev"],
Self::Local => vec!["http://localhost:8080"],
}
}
pub fn allow_http(&self) -> bool {
matches!(self, Self::Local)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LookupQuestion {
pub service: String,
pub query: serde_json::Value,
}
impl LookupQuestion {
pub fn new(service: impl Into<String>, query: serde_json::Value) -> Self {
Self {
service: service.into(),
query,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LookupAnswerType {
OutputList,
Freeform,
Formula,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputListItem {
pub beef: Vec<u8>,
#[serde(rename = "outputIndex")]
pub output_index: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LookupFormula {
pub outpoint: String,
#[serde(rename = "historyFn")]
pub history_fn: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum LookupAnswer {
#[serde(rename = "output-list")]
OutputList {
outputs: Vec<OutputListItem>,
},
#[serde(rename = "freeform")]
Freeform {
result: serde_json::Value,
},
#[serde(rename = "formula")]
Formula {
formulas: Vec<LookupFormula>,
},
}
impl LookupAnswer {
pub fn answer_type(&self) -> LookupAnswerType {
match self {
Self::OutputList { .. } => LookupAnswerType::OutputList,
Self::Freeform { .. } => LookupAnswerType::Freeform,
Self::Formula { .. } => LookupAnswerType::Formula,
}
}
pub fn empty_output_list() -> Self {
Self::OutputList {
outputs: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaggedBEEF {
pub beef: Vec<u8>,
pub topics: Vec<String>,
#[serde(rename = "offChainValues", skip_serializing_if = "Option::is_none")]
pub off_chain_values: Option<Vec<u8>>,
}
impl TaggedBEEF {
pub fn new(beef: Vec<u8>, topics: Vec<String>) -> Self {
Self {
beef,
topics,
off_chain_values: None,
}
}
pub fn with_off_chain_values(beef: Vec<u8>, topics: Vec<String>, off_chain: Vec<u8>) -> Self {
Self {
beef,
topics,
off_chain_values: Some(off_chain),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AdmittanceInstructions {
#[serde(rename = "outputsToAdmit")]
pub outputs_to_admit: Vec<u32>,
#[serde(rename = "coinsToRetain")]
pub coins_to_retain: Vec<u32>,
#[serde(rename = "coinsRemoved", skip_serializing_if = "Option::is_none")]
pub coins_removed: Option<Vec<u32>>,
}
impl AdmittanceInstructions {
pub fn has_activity(&self) -> bool {
!self.outputs_to_admit.is_empty()
|| !self.coins_to_retain.is_empty()
|| self.coins_removed.as_ref().is_some_and(|v| !v.is_empty())
}
}
pub type Steak = HashMap<String, AdmittanceInstructions>;
#[derive(Debug, Clone)]
pub struct HostResponse {
pub host: String,
pub success: bool,
pub steak: Option<Steak>,
pub error: Option<String>,
}
impl HostResponse {
pub fn success(host: String, steak: Steak) -> Self {
Self {
host,
success: true,
steak: Some(steak),
error: None,
}
}
pub fn failure(host: String, error: String) -> Self {
Self {
host,
success: false,
steak: None,
error: Some(error),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServiceMetadata {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "iconUrl", skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(rename = "infoUrl", skip_serializing_if = "Option::is_none")]
pub info_url: Option<String>,
}
pub const MAX_TRACKER_WAIT_TIME_MS: u64 = 5000;
pub const MAX_SHIP_QUERY_TIMEOUT_MS: u64 = 5000;
pub const DEFAULT_HOSTS_CACHE_TTL_MS: u64 = 5 * 60 * 1000;
pub const DEFAULT_HOSTS_CACHE_MAX_ENTRIES: usize = 128;
pub const DEFAULT_TX_MEMO_TTL_MS: u64 = 10 * 60 * 1000;
pub const DEFAULT_TX_MEMO_MAX_ENTRIES: usize = 4096;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_protocol_str_roundtrip() {
assert_eq!(Protocol::parse("SHIP"), Some(Protocol::Ship));
assert_eq!(Protocol::parse("SLAP"), Some(Protocol::Slap));
assert_eq!(Protocol::parse("ship"), Some(Protocol::Ship));
assert_eq!(Protocol::parse("other"), None);
assert_eq!(Protocol::Ship.as_str(), "SHIP");
assert_eq!(Protocol::Slap.as_str(), "SLAP");
}
#[test]
fn test_network_preset_slap_trackers() {
let mainnet = NetworkPreset::Mainnet.slap_trackers();
assert!(!mainnet.is_empty());
assert!(mainnet[0].starts_with("https://"));
let local = NetworkPreset::Local.slap_trackers();
assert!(local[0].starts_with("http://"));
}
#[test]
fn test_network_preset_allow_http() {
assert!(!NetworkPreset::Mainnet.allow_http());
assert!(!NetworkPreset::Testnet.allow_http());
assert!(NetworkPreset::Local.allow_http());
}
#[test]
fn test_lookup_question_new() {
let q = LookupQuestion::new("ls_test", serde_json::json!({"key": "value"}));
assert_eq!(q.service, "ls_test");
assert_eq!(q.query["key"], "value");
}
#[test]
fn test_lookup_answer_type() {
let output_list = LookupAnswer::OutputList { outputs: vec![] };
assert_eq!(output_list.answer_type(), LookupAnswerType::OutputList);
let freeform = LookupAnswer::Freeform {
result: serde_json::Value::Null,
};
assert_eq!(freeform.answer_type(), LookupAnswerType::Freeform);
}
#[test]
fn test_tagged_beef() {
let beef = TaggedBEEF::new(vec![1, 2, 3], vec!["tm_test".to_string()]);
assert_eq!(beef.beef, vec![1, 2, 3]);
assert_eq!(beef.topics, vec!["tm_test"]);
assert!(beef.off_chain_values.is_none());
let beef_with_off_chain = TaggedBEEF::with_off_chain_values(
vec![1, 2, 3],
vec!["tm_test".to_string()],
vec![4, 5],
);
assert!(beef_with_off_chain.off_chain_values.is_some());
}
#[test]
fn test_admittance_instructions_has_activity() {
let empty = AdmittanceInstructions::default();
assert!(!empty.has_activity());
let with_admits = AdmittanceInstructions {
outputs_to_admit: vec![0],
..Default::default()
};
assert!(with_admits.has_activity());
let with_retains = AdmittanceInstructions {
coins_to_retain: vec![0],
..Default::default()
};
assert!(with_retains.has_activity());
}
#[test]
fn test_host_response() {
let success = HostResponse::success("https://example.com".to_string(), HashMap::new());
assert!(success.success);
assert!(success.steak.is_some());
assert!(success.error.is_none());
let failure = HostResponse::failure(
"https://example.com".to_string(),
"Connection refused".to_string(),
);
assert!(!failure.success);
assert!(failure.steak.is_none());
assert!(failure.error.is_some());
}
}