use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use bsv_rs::overlay::types::{
AdmittanceInstructions, HostResponse, LookupAnswer, LookupAnswerType, LookupFormula,
LookupQuestion, NetworkPreset, OutputListItem, Protocol, ServiceMetadata, Steak, TaggedBEEF,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Outpoint {
pub txid: String,
#[serde(rename = "outputIndex")]
pub output_index: u32,
}
impl Outpoint {
pub fn new(txid: impl Into<String>, output_index: u32) -> Self {
Self {
txid: txid.into(),
output_index,
}
}
pub fn from_graph_id(graph_id: &str) -> Option<Self> {
let parts: Vec<&str> = graph_id.splitn(2, '.').collect();
if parts.len() != 2 {
return None;
}
let output_index = parts[1].parse::<u32>().ok()?;
Some(Self {
txid: parts[0].to_string(),
output_index,
})
}
pub fn to_graph_id(&self) -> String {
format!("{}.{}", self.txid, self.output_index)
}
}
impl std::fmt::Display for Outpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.txid, self.output_index)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Output {
pub txid: String,
#[serde(rename = "outputIndex")]
pub output_index: u32,
#[serde(rename = "outputScript")]
pub output_script: Vec<u8>,
pub satoshis: u64,
pub topic: String,
pub spent: bool,
#[serde(rename = "outputsConsumed", default)]
pub outputs_consumed: Vec<Outpoint>,
#[serde(rename = "consumedBy", default)]
pub consumed_by: Vec<Outpoint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub beef: Option<Vec<u8>>,
#[serde(rename = "blockHeight", skip_serializing_if = "Option::is_none")]
pub block_height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score: Option<f64>,
}
impl Output {
pub fn outpoint(&self) -> Outpoint {
Outpoint::new(&self.txid, self.output_index)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Advertisement {
pub protocol: Protocol,
#[serde(rename = "identityKey")]
pub identity_key: String,
pub domain: String,
#[serde(rename = "topicOrService")]
pub topic_or_service: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub beef: Option<Vec<u8>>,
#[serde(rename = "outputIndex", skip_serializing_if = "Option::is_none")]
pub output_index: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvertisementData {
pub protocol: Protocol,
#[serde(rename = "topicOrServiceName")]
pub topic_or_service_name: String,
}
pub type SyncConfiguration = HashMap<String, SyncTarget>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SyncTarget {
Ship,
Peers(Vec<String>),
Disabled,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TopicAdmittanceContext {
pub dry_run: bool,
}
impl TopicAdmittanceContext {
pub const DRY_RUN: Self = Self { dry_run: true };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SubmitMode {
#[default]
#[serde(rename = "current-tx")]
CurrentTx,
#[serde(rename = "historical-tx")]
HistoricalTx,
#[serde(rename = "historical-tx-no-spv")]
HistoricalTxNoSpv,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AdmissionMode {
#[serde(rename = "locking-script")]
LockingScript,
#[serde(rename = "whole-tx")]
WholeTx,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpendNotificationMode {
#[serde(rename = "none")]
None,
#[serde(rename = "txid")]
Txid,
#[serde(rename = "script")]
Script,
#[serde(rename = "whole-tx")]
WholeTx,
}
#[derive(Debug, Clone)]
pub enum OutputAdmittedByTopic {
LockingScript {
txid: String,
output_index: u32,
topic: String,
satoshis: u64,
locking_script: Vec<u8>,
off_chain_values: Option<Vec<u8>>,
},
WholeTx {
atomic_beef: Vec<u8>,
output_index: u32,
topic: String,
off_chain_values: Option<Vec<u8>>,
},
}
#[derive(Debug, Clone)]
pub enum OutputSpent {
None {
txid: String,
output_index: u32,
topic: String,
},
Txid {
txid: String,
output_index: u32,
topic: String,
spending_txid: String,
},
Script {
txid: String,
output_index: u32,
topic: String,
spending_txid: String,
input_index: u32,
unlocking_script: Vec<u8>,
sequence_number: u32,
off_chain_values: Option<Vec<u8>>,
},
WholeTx {
txid: String,
output_index: u32,
topic: String,
spending_atomic_beef: Vec<u8>,
off_chain_values: Option<Vec<u8>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPInitialRequest {
pub version: u32,
pub since: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPOutput {
pub txid: String,
#[serde(rename = "outputIndex")]
pub output_index: u32,
pub score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPInitialResponse {
#[serde(rename = "UTXOList")]
pub utxo_list: Vec<GASPOutput>,
pub since: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPInitialReply {
#[serde(rename = "UTXOList")]
pub utxo_list: Vec<GASPOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPNode {
#[serde(rename = "graphID")]
pub graph_id: String,
#[serde(rename = "rawTx")]
pub raw_tx: String,
#[serde(rename = "outputIndex")]
pub output_index: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub proof: Option<String>,
#[serde(rename = "txMetadata", skip_serializing_if = "Option::is_none")]
pub tx_metadata: Option<String>,
#[serde(rename = "outputMetadata", skip_serializing_if = "Option::is_none")]
pub output_metadata: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<HashMap<String, GASPInputRef>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPInputRef {
pub hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPNodeResponse {
#[serde(rename = "requestedInputs")]
pub requested_inputs: HashMap<String, GASPInputRequest>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GASPInputRequest {
pub metadata: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedTransaction {
pub txid: String,
pub topic: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UTXOReference {
pub txid: String,
#[serde(rename = "outputIndex")]
pub output_index: u32,
}
#[derive(Debug, Clone)]
pub enum LookupResult {
OutputList(Vec<UTXOReference>),
Answer(LookupAnswer),
}
impl LookupResult {
pub fn outputs(refs: Vec<UTXOReference>) -> Self {
Self::OutputList(refs)
}
pub fn answer(answer: LookupAnswer) -> Self {
Self::Answer(answer)
}
pub fn as_outputs(&self) -> Option<&[UTXOReference]> {
match self {
Self::OutputList(refs) => Some(refs),
Self::Answer(_) => None,
}
}
pub fn into_outputs(self) -> Option<Vec<UTXOReference>> {
match self {
Self::OutputList(refs) => Some(refs),
Self::Answer(_) => None,
}
}
}
impl From<Vec<UTXOReference>> for LookupResult {
fn from(refs: Vec<UTXOReference>) -> Self {
Self::OutputList(refs)
}
}
impl From<LookupAnswer> for LookupResult {
fn from(answer: LookupAnswer) -> Self {
Self::Answer(answer)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_outpoint_graph_id_roundtrip() {
let op = Outpoint::new("abc123", 2);
let graph_id = op.to_graph_id();
assert_eq!(graph_id, "abc123.2");
let parsed = Outpoint::from_graph_id(&graph_id).unwrap();
assert_eq!(parsed, op);
}
#[test]
fn test_outpoint_from_graph_id_invalid() {
assert!(Outpoint::from_graph_id("no_dot").is_none());
assert!(Outpoint::from_graph_id("abc.notanum").is_none());
}
#[test]
fn test_output_serde_roundtrip() {
let output = Output {
txid: "deadbeef".to_string(),
output_index: 0,
output_script: vec![0x76, 0xa9],
satoshis: 1000,
topic: "tm_test".to_string(),
spent: false,
outputs_consumed: vec![Outpoint::new("prev", 1)],
consumed_by: vec![],
beef: None,
block_height: Some(800_000),
score: Some(1.0),
};
let json = serde_json::to_string(&output).unwrap();
let back: Output = serde_json::from_str(&json).unwrap();
assert_eq!(back.txid, "deadbeef");
assert_eq!(back.output_index, 0);
assert_eq!(back.satoshis, 1000);
assert_eq!(back.topic, "tm_test");
assert!(!back.spent);
assert_eq!(back.outputs_consumed.len(), 1);
assert_eq!(back.block_height, Some(800_000));
}
#[test]
fn test_advertisement_serde() {
let ad = Advertisement {
protocol: Protocol::Ship,
identity_key: "02abc".to_string(),
domain: "https://example.com".to_string(),
topic_or_service: "tm_test".to_string(),
beef: None,
output_index: None,
};
let json = serde_json::to_string(&ad).unwrap();
assert!(json.contains("SHIP"));
let back: Advertisement = serde_json::from_str(&json).unwrap();
assert_eq!(back.protocol, Protocol::Ship);
assert_eq!(back.topic_or_service, "tm_test");
}
#[test]
fn test_submit_mode_serde() {
let json = serde_json::to_string(&SubmitMode::HistoricalTxNoSpv).unwrap();
assert_eq!(json, "\"historical-tx-no-spv\"");
let back: SubmitMode = serde_json::from_str(&json).unwrap();
assert_eq!(back, SubmitMode::HistoricalTxNoSpv);
}
#[test]
fn test_gasp_initial_request_serde() {
let req = GASPInitialRequest {
version: 1,
since: 1_700_000_000,
limit: Some(10_000),
};
let json = serde_json::to_string(&req).unwrap();
let back: GASPInitialRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.version, 1);
assert_eq!(back.since, 1_700_000_000);
assert_eq!(back.limit, Some(10_000));
}
#[test]
fn test_gasp_node_serde() {
let node = GASPNode {
graph_id: "abc.0".to_string(),
raw_tx: "0100000001".to_string(),
output_index: 0,
proof: None,
tx_metadata: None,
output_metadata: None,
inputs: None,
};
let json = serde_json::to_string(&node).unwrap();
assert!(json.contains("graphID"));
assert!(json.contains("rawTx"));
let back: GASPNode = serde_json::from_str(&json).unwrap();
assert_eq!(back.graph_id, "abc.0");
}
#[test]
fn test_utxo_reference_serde() {
let r = UTXOReference {
txid: "dead".to_string(),
output_index: 3,
};
let json = serde_json::to_string(&r).unwrap();
assert!(json.contains("outputIndex"));
let back: UTXOReference = serde_json::from_str(&json).unwrap();
assert_eq!(back.output_index, 3);
}
#[test]
fn test_admission_mode_serde() {
let json = serde_json::to_string(&AdmissionMode::LockingScript).unwrap();
assert_eq!(json, "\"locking-script\"");
}
#[test]
fn test_spend_notification_mode_serde() {
let json = serde_json::to_string(&SpendNotificationMode::WholeTx).unwrap();
assert_eq!(json, "\"whole-tx\"");
}
}