1use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
8pub enum Source {
9 Swan,
10 Coinbase,
11 Gemini,
12 River,
13}
14impl Source {
15 pub fn priority(self) -> u8 {
17 match self {
18 Source::Swan => 0,
19 Source::Coinbase => 1,
20 Source::Gemini => 2,
21 Source::River => 3,
22 }
23 }
24 pub fn tag(self) -> &'static str {
25 match self {
26 Source::Swan => "swan",
27 Source::Coinbase => "coinbase",
28 Source::Gemini => "gemini",
29 Source::River => "river",
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36pub struct SourceRef(pub String);
37impl SourceRef {
38 pub fn new(s: impl Into<String>) -> Self {
39 SourceRef(s.into())
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
45pub struct Fingerprint(pub String);
46impl Fingerprint {
47 pub fn of_bytes(bytes: &[u8]) -> Self {
48 let mut h = Sha256::new();
49 h.update(bytes);
50 Fingerprint(format!("{:x}", h.finalize()))
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
56pub enum EventId {
57 Import {
59 source: Source,
60 source_ref: SourceRef,
61 },
62 Conflict {
64 source: Source,
65 source_ref: SourceRef,
66 fingerprint: Fingerprint,
67 },
68 Decision { seq: u64 },
70}
71impl EventId {
72 pub fn import(source: Source, source_ref: SourceRef) -> Self {
73 EventId::Import { source, source_ref }
74 }
75 pub fn conflict(source: Source, source_ref: SourceRef, fingerprint: &Fingerprint) -> Self {
76 EventId::Conflict {
77 source,
78 source_ref,
79 fingerprint: fingerprint.clone(),
80 }
81 }
82 pub fn decision(seq: u64) -> Self {
83 EventId::Decision { seq }
84 }
85 pub fn canonical(&self) -> String {
87 match self {
88 EventId::Import { source, source_ref } => {
89 format!("import|{}|{}", source.tag(), source_ref.0)
90 }
91 EventId::Conflict {
92 source,
93 source_ref,
94 fingerprint,
95 } => {
96 format!(
97 "conflict|{}|{}|{}",
98 source.tag(),
99 source_ref.0,
100 fingerprint.0
101 )
102 }
103 EventId::Decision { seq } => format!("decision|{seq}"),
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
110pub enum WalletId {
111 Exchange { provider: String, account: String },
112 SelfCustody { label: String },
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
117pub struct LotId {
118 pub origin_event_id: EventId,
119 pub split_sequence: u32,
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn source_priority_is_swan_first_river_last() {
128 let mut v = [
129 Source::River,
130 Source::Coinbase,
131 Source::Swan,
132 Source::Gemini,
133 ];
134 v.sort_by_key(|s| s.priority());
135 assert_eq!(
136 v,
137 [
138 Source::Swan,
139 Source::Coinbase,
140 Source::Gemini,
141 Source::River
142 ]
143 );
144 }
145
146 #[test]
147 fn import_event_id_is_stable_function_of_source_and_ref() {
148 let a = EventId::import(Source::Coinbase, SourceRef::new("ID-1"));
149 let b = EventId::import(Source::Coinbase, SourceRef::new("ID-1"));
150 assert_eq!(a, b);
151 assert_eq!(a.canonical(), "import|coinbase|ID-1");
152 }
153
154 #[test]
155 fn conflict_event_id_is_distinct_from_its_target() {
156 let target = EventId::import(Source::Gemini, SourceRef::new("T1"));
157 let fp = Fingerprint::of_bytes(b"new-content");
158 let c1 = EventId::conflict(Source::Gemini, SourceRef::new("T1"), &fp);
159 let c2 = EventId::conflict(Source::Gemini, SourceRef::new("T1"), &fp);
160 assert_ne!(EventId::import(Source::Gemini, SourceRef::new("T1")), c1);
161 assert_eq!(c1, c2); let _ = target;
163 }
164
165 #[test]
166 fn decision_event_id_is_function_of_seq() {
167 assert_eq!(EventId::decision(7).canonical(), "decision|7");
168 assert_ne!(EventId::decision(7), EventId::decision(8));
169 }
170
171 #[test]
172 fn lot_id_is_origin_plus_split_sequence() {
173 let origin = EventId::import(Source::Swan, SourceRef::new("TX9"));
174 let l0 = LotId {
175 origin_event_id: origin.clone(),
176 split_sequence: 0,
177 };
178 let l1 = LotId {
179 origin_event_id: origin,
180 split_sequence: 1,
181 };
182 assert_ne!(l0, l1);
183 assert!(l0 < l1); }
185}