use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Source {
Swan,
Coinbase,
Gemini,
River,
}
impl Source {
pub fn priority(self) -> u8 {
match self {
Source::Swan => 0,
Source::Coinbase => 1,
Source::Gemini => 2,
Source::River => 3,
}
}
pub fn tag(self) -> &'static str {
match self {
Source::Swan => "swan",
Source::Coinbase => "coinbase",
Source::Gemini => "gemini",
Source::River => "river",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SourceRef(pub String);
impl SourceRef {
pub fn new(s: impl Into<String>) -> Self {
SourceRef(s.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Fingerprint(pub String);
impl Fingerprint {
pub fn of_bytes(bytes: &[u8]) -> Self {
let mut h = Sha256::new();
h.update(bytes);
Fingerprint(format!("{:x}", h.finalize()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum EventId {
Import {
source: Source,
source_ref: SourceRef,
},
Conflict {
source: Source,
source_ref: SourceRef,
fingerprint: Fingerprint,
},
Decision { seq: u64 },
}
impl EventId {
pub fn import(source: Source, source_ref: SourceRef) -> Self {
EventId::Import { source, source_ref }
}
pub fn conflict(source: Source, source_ref: SourceRef, fingerprint: &Fingerprint) -> Self {
EventId::Conflict {
source,
source_ref,
fingerprint: fingerprint.clone(),
}
}
pub fn decision(seq: u64) -> Self {
EventId::Decision { seq }
}
pub fn canonical(&self) -> String {
match self {
EventId::Import { source, source_ref } => {
format!("import|{}|{}", source.tag(), source_ref.0)
}
EventId::Conflict {
source,
source_ref,
fingerprint,
} => {
format!(
"conflict|{}|{}|{}",
source.tag(),
source_ref.0,
fingerprint.0
)
}
EventId::Decision { seq } => format!("decision|{seq}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum WalletId {
Exchange { provider: String, account: String },
SelfCustody { label: String },
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct LotId {
pub origin_event_id: EventId,
pub split_sequence: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_priority_is_swan_first_river_last() {
let mut v = [
Source::River,
Source::Coinbase,
Source::Swan,
Source::Gemini,
];
v.sort_by_key(|s| s.priority());
assert_eq!(
v,
[
Source::Swan,
Source::Coinbase,
Source::Gemini,
Source::River
]
);
}
#[test]
fn import_event_id_is_stable_function_of_source_and_ref() {
let a = EventId::import(Source::Coinbase, SourceRef::new("ID-1"));
let b = EventId::import(Source::Coinbase, SourceRef::new("ID-1"));
assert_eq!(a, b);
assert_eq!(a.canonical(), "import|coinbase|ID-1");
}
#[test]
fn conflict_event_id_is_distinct_from_its_target() {
let target = EventId::import(Source::Gemini, SourceRef::new("T1"));
let fp = Fingerprint::of_bytes(b"new-content");
let c1 = EventId::conflict(Source::Gemini, SourceRef::new("T1"), &fp);
let c2 = EventId::conflict(Source::Gemini, SourceRef::new("T1"), &fp);
assert_ne!(EventId::import(Source::Gemini, SourceRef::new("T1")), c1);
assert_eq!(c1, c2); let _ = target;
}
#[test]
fn decision_event_id_is_function_of_seq() {
assert_eq!(EventId::decision(7).canonical(), "decision|7");
assert_ne!(EventId::decision(7), EventId::decision(8));
}
#[test]
fn lot_id_is_origin_plus_split_sequence() {
let origin = EventId::import(Source::Swan, SourceRef::new("TX9"));
let l0 = LotId {
origin_event_id: origin.clone(),
split_sequence: 0,
};
let l1 = LotId {
origin_event_id: origin,
split_sequence: 1,
};
assert_ne!(l0, l1);
assert!(l0 < l1); }
}