1use std::collections::HashMap;
2use std::hash::Hash;
3
4use bdk_core::Merge;
5use bdk_esplora::esplora_client::Amount;
6use bitcoin::SignedAmount;
7use chrono::DateTime;
8
9use ark::vtxo::VtxoRef;
10use ark::VtxoId;
11
12use crate::movement::{Movement, MovementDestination};
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
16enum UpdateMethod<T> {
17 Merge(T),
19 Replace(T),
21}
22
23#[derive(Debug, Clone)]
33pub struct MovementUpdate {
34 intended_balance: Option<SignedAmount>,
35 effective_balance: Option<SignedAmount>,
36 offchain_fee: Option<Amount>,
37 sent_to: Option<UpdateMethod<Vec<MovementDestination>>>,
38 received_on: Option<UpdateMethod<Vec<MovementDestination>>>,
39 consumed_vtxos: Option<UpdateMethod<Vec<VtxoId>>>,
40 produced_vtxos: Option<UpdateMethod<Vec<VtxoId>>>,
41 exited_vtxos: Option<UpdateMethod<Vec<VtxoId>>>,
42 metadata: Option<UpdateMethod<HashMap<String, serde_json::Value>>>,
43}
44
45impl MovementUpdate {
46 pub fn new() -> Self {
47 Self {
48 intended_balance: None,
49 effective_balance: None,
50 offchain_fee: None,
51 sent_to: None,
52 received_on: None,
53 consumed_vtxos: None,
54 produced_vtxos: None,
55 exited_vtxos: None,
56 metadata: None,
57 }
58 }
59
60 pub fn consumed_vtxo(self, vtxo: impl VtxoRef) -> Self {
61 self.consumed_vtxos([vtxo.vtxo_id()])
62 }
63
64 pub fn consumed_vtxo_if_some(self, vtxo: Option<impl VtxoRef>) -> Self {
65 if let Some(vtxo) = vtxo {
66 self.consumed_vtxo(vtxo)
67 } else {
68 self
69 }
70 }
71
72 pub fn consumed_vtxos(mut self, vtxos: impl IntoIterator<Item = impl VtxoRef>) -> Self {
73 let vtxos = vtxos.into_iter().map(|vtxo| vtxo.vtxo_id());
74 match &mut self.consumed_vtxos {
75 None => self.consumed_vtxos = Some(UpdateMethod::Merge(vtxos.collect())),
76 Some(vec) => vec.merge(vtxos),
77 }
78 self
79 }
80
81 pub fn effective_balance(mut self, effective: SignedAmount) -> Self {
82 self.effective_balance = Some(effective);
83 self
84 }
85
86 pub fn exited_vtxo(self, vtxo: impl VtxoRef) -> Self {
87 self.exited_vtxos([vtxo.vtxo_id()])
88 }
89
90 pub fn exited_vtxo_if_some(self, vtxo: Option<impl VtxoRef>) -> Self {
91 if let Some(vtxo) = vtxo {
92 self.exited_vtxo(vtxo)
93 } else {
94 self
95 }
96 }
97
98 pub fn exited_vtxos(mut self, vtxos: impl IntoIterator<Item = impl VtxoRef>) -> Self {
99 let vtxos = vtxos.into_iter().map(|vtxo| vtxo.vtxo_id());
100 match &mut self.exited_vtxos {
101 None => self.exited_vtxos = Some(UpdateMethod::Merge(vtxos.collect())),
102 Some(vec) => vec.merge(vtxos),
103 }
104 self
105 }
106
107 pub fn fee(mut self, offchain_fee: Amount) -> Self {
108 self.offchain_fee = Some(offchain_fee);
109 self
110 }
111
112 pub fn intended_balance(mut self, intended: SignedAmount) -> Self {
113 self.intended_balance = Some(intended);
114 self
115 }
116
117 pub fn intended_and_effective_balance(mut self, balance: SignedAmount) -> Self {
118 self.intended_balance = Some(balance);
119 self.effective_balance = Some(balance);
120 self
121 }
122
123 pub fn metadata(
124 mut self,
125 metadata: impl IntoIterator<Item = (String, serde_json::Value)>,
126 ) -> Self {
127 match &mut self.metadata {
128 None => self.metadata = Some(UpdateMethod::Merge(metadata.into_iter().collect())),
129 Some(map) => map.insert(metadata),
130 }
131 self
132 }
133
134 pub fn produced_vtxo(self, vtxo: impl VtxoRef) -> Self {
135 self.produced_vtxos([vtxo])
136 }
137
138 pub fn produced_vtxo_if_some(self, vtxo: Option<impl VtxoRef>) -> Self {
139 if let Some(vtxo) = vtxo {
140 self.produced_vtxo(vtxo)
141 } else {
142 self
143 }
144 }
145
146 pub fn produced_vtxos(mut self, vtxos: impl IntoIterator<Item = impl VtxoRef>) -> Self {
147 let vtxos = vtxos.into_iter().map(|v| v.vtxo_id());
148 match &mut self.produced_vtxos {
149 None => self.produced_vtxos = Some(UpdateMethod::Merge(vtxos.collect())),
150 Some(vec) => vec.merge(vtxos),
151 }
152 self
153 }
154
155 pub fn received_on(mut self, received: impl IntoIterator<Item = MovementDestination>) -> Self {
156 match &mut self.received_on {
157 None => self.received_on = Some(UpdateMethod::Merge(received.into_iter().collect())),
158 Some(vec) => vec.merge(received),
159 }
160 self
161 }
162
163 pub fn sent_to(mut self, destinations: impl IntoIterator<Item = MovementDestination>) -> Self {
164 match &mut self.sent_to {
165 None => self.sent_to = Some(UpdateMethod::Merge(destinations.into_iter().collect())),
166 Some(vec) => vec.merge(destinations),
167 }
168 self
169 }
170
171 pub fn replace_consumed_vtxos(mut self, vtxos: impl IntoIterator<Item = impl VtxoRef>) -> Self {
172 self.consumed_vtxos = Some(UpdateMethod::Replace(
173 vtxos.into_iter().map(|v| v.vtxo_id()).collect(),
174 ));
175 self
176 }
177
178 pub fn replace_exited_vtxos(mut self, vtxos: impl IntoIterator<Item = impl VtxoRef>) -> Self {
179 self.exited_vtxos = Some(UpdateMethod::Replace(
180 vtxos.into_iter().map(|v| v.vtxo_id()).collect(),
181 ));
182 self
183 }
184
185 pub fn replace_metadata(
186 mut self,
187 metadata: impl IntoIterator<Item = (String, serde_json::Value)>,
188 ) -> Self {
189 self.metadata = Some(UpdateMethod::Replace(metadata.into_iter().collect()));
190 self
191 }
192
193 pub fn replace_produced_vtxos(mut self, vtxos: impl IntoIterator<Item = impl VtxoRef>) -> Self {
194 self.produced_vtxos = Some(UpdateMethod::Replace(
195 vtxos.into_iter().map(|v| v.vtxo_id()).collect(),
196 ));
197 self
198 }
199
200 pub fn replace_received_on(
201 mut self,
202 received: impl IntoIterator<Item = MovementDestination>,
203 ) -> Self {
204 self.received_on = Some(UpdateMethod::Replace(
205 received.into_iter().collect(),
206 ));
207 self
208 }
209
210 pub fn replace_sent_on(
211 mut self,
212 destinations: impl IntoIterator<Item = MovementDestination>,
213 ) -> Self {
214 self.sent_to = Some(UpdateMethod::Replace(
215 destinations.into_iter().collect(),
216 ));
217 self
218 }
219
220 pub fn apply_to(self, movement: &mut Movement, at: DateTime<chrono::Local>) {
221 movement.time.updated_at = at;
222 if let Some(metadata) = self.metadata {
223 metadata.apply_to(&mut movement.metadata);
224 }
225 if let Some(intended) = self.intended_balance {
226 movement.intended_balance = intended;
227 }
228 if let Some(effective) = self.effective_balance {
229 movement.effective_balance = effective;
230 }
231 if let Some(offchain_fee) = self.offchain_fee {
232 movement.offchain_fee = offchain_fee;
233 }
234 if let Some(sent_to) = self.sent_to {
235 sent_to.apply_to(&mut movement.sent_to);
236 }
237 if let Some(received_on) = self.received_on {
238 received_on.apply_to(&mut movement.received_on);
239 }
240 if let Some(input_vtxos) = self.consumed_vtxos {
241 input_vtxos.apply_unique_to(&mut movement.input_vtxos);
242 }
243 if let Some(output_vtxos) = self.produced_vtxos {
244 output_vtxos.apply_unique_to(&mut movement.output_vtxos);
245 }
246 if let Some(exited_vtxos) = self.exited_vtxos {
247 exited_vtxos.apply_unique_to(&mut movement.exited_vtxos);
248 }
249 }
250}
251
252impl<T: PartialEq + Eq + Ord> UpdateMethod<Vec<T>> {
253 pub fn apply_to(self, target: &mut Vec<T>) {
254 match self {
255 UpdateMethod::Merge(vec) => target.extend(vec),
256 UpdateMethod::Replace(vec) => *target = vec,
257 }
258 }
259
260 pub fn apply_unique_to(self, target: &mut Vec<T>) {
261 match self {
262 UpdateMethod::Merge(vec) => target.merge(vec),
263 UpdateMethod::Replace(vec) => *target = vec,
264 }
265 target.sort_unstable();
266 target.dedup();
267 }
268
269 pub fn merge(&mut self, values: impl IntoIterator<Item = T>) {
270 let vec = match self {
271 UpdateMethod::Merge(vec) => vec,
272 UpdateMethod::Replace(vec) => vec,
273 };
274 values.into_iter().for_each(|value| vec.push(value));
275 }
276}
277
278impl<K: Eq + Hash, V> UpdateMethod<HashMap<K, V>> {
279 pub fn apply_to(self, target: &mut HashMap<K, V>) {
280 match self {
281 UpdateMethod::Merge(map) => target.extend(map),
282 UpdateMethod::Replace(map) => *target = map,
283 }
284 }
285
286 pub fn insert(&mut self, key_pairs: impl IntoIterator<Item = (K, V)>) {
287 let map = match self {
288 UpdateMethod::Merge(map) => map,
289 UpdateMethod::Replace(map) => map,
290 };
291 for (key, value) in key_pairs {
292 map.insert(key, value);
293 }
294 }
295}