1use chrono::Utc;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use tap_caip::AssetId;
10
11use crate::didcomm::PlainMessage;
12use crate::error::{Error, Result};
13use crate::impl_tap_message;
14use crate::message::tap_message_trait::{Authorizable, Connectable, TapMessageBody};
15use crate::message::{Authorize, Participant, Policy, RemoveAgent, ReplaceAgent, UpdatePolicies};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Transfer {
20 pub asset: AssetId,
22
23 #[serde(rename = "originator")]
25 pub originator: Participant,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub beneficiary: Option<Participant>,
30
31 pub amount: String,
33
34 #[serde(default)]
36 pub agents: Vec<Participant>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub memo: Option<String>,
41
42 #[serde(rename = "settlementId", skip_serializing_if = "Option::is_none")]
44 pub settlement_id: Option<String>,
45
46 #[serde(skip)]
48 pub transaction_id: String,
49
50 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
52 pub metadata: HashMap<String, serde_json::Value>,
53}
54
55impl Transfer {
56 pub fn builder() -> TransferBuilder {
85 TransferBuilder::default()
86 }
87
88 pub fn message_id(&self) -> String {
90 uuid::Uuid::new_v4().to_string()
91 }
92
93 pub fn validate(&self) -> Result<()> {
95 if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
98 return Err(Error::Validation("Asset ID is invalid".to_string()));
99 }
100
101 if self.originator.id.is_empty() {
103 return Err(Error::Validation("Originator ID is required".to_string()));
104 }
105
106 if self.amount.is_empty() {
108 return Err(Error::Validation("Amount is required".to_string()));
109 }
110
111 match self.amount.parse::<f64>() {
113 Ok(amount) if amount <= 0.0 => {
114 return Err(Error::Validation("Amount must be positive".to_string()));
115 }
116 Err(_) => {
117 return Err(Error::Validation(
118 "Amount must be a valid number".to_string(),
119 ));
120 }
121 _ => {}
122 }
123
124 for agent in &self.agents {
126 if agent.id.is_empty() {
127 return Err(Error::Validation("Agent ID cannot be empty".to_string()));
128 }
129 }
130
131 Ok(())
132 }
133}
134
135#[derive(Default)]
137pub struct TransferBuilder {
138 asset: Option<AssetId>,
139 originator: Option<Participant>,
140 amount: Option<String>,
141 beneficiary: Option<Participant>,
142 settlement_id: Option<String>,
143 memo: Option<String>,
144 transaction_id: Option<String>,
145 agents: Vec<Participant>,
146 metadata: HashMap<String, serde_json::Value>,
147}
148
149impl TransferBuilder {
150 pub fn asset(mut self, asset: AssetId) -> Self {
152 self.asset = Some(asset);
153 self
154 }
155
156 pub fn originator(mut self, originator: Participant) -> Self {
158 self.originator = Some(originator);
159 self
160 }
161
162 pub fn amount(mut self, amount: String) -> Self {
164 self.amount = Some(amount);
165 self
166 }
167
168 pub fn beneficiary(mut self, beneficiary: Participant) -> Self {
170 self.beneficiary = Some(beneficiary);
171 self
172 }
173
174 pub fn settlement_id(mut self, settlement_id: String) -> Self {
176 self.settlement_id = Some(settlement_id);
177 self
178 }
179
180 pub fn memo(mut self, memo: String) -> Self {
182 self.memo = Some(memo);
183 self
184 }
185
186 pub fn transaction_id(mut self, transaction_id: String) -> Self {
188 self.transaction_id = Some(transaction_id);
189 self
190 }
191
192 pub fn add_agent(mut self, agent: Participant) -> Self {
194 self.agents.push(agent);
195 self
196 }
197
198 pub fn agents(mut self, agents: Vec<Participant>) -> Self {
200 self.agents = agents;
201 self
202 }
203
204 pub fn add_metadata(mut self, key: String, value: serde_json::Value) -> Self {
206 self.metadata.insert(key, value);
207 self
208 }
209
210 pub fn metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
212 self.metadata = metadata;
213 self
214 }
215
216 pub fn build(self) -> Transfer {
222 Transfer {
223 asset: self.asset.expect("Asset is required"),
224 originator: self.originator.expect("Originator is required"),
225 amount: self.amount.expect("Amount is required"),
226 beneficiary: self.beneficiary,
227 settlement_id: self.settlement_id,
228 memo: self.memo,
229 transaction_id: self
230 .transaction_id
231 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
232 agents: self.agents,
233 metadata: self.metadata,
234 }
235 }
236
237 pub fn try_build(self) -> Result<Transfer> {
239 let asset = self
240 .asset
241 .ok_or_else(|| Error::Validation("Asset is required".to_string()))?;
242 let originator = self
243 .originator
244 .ok_or_else(|| Error::Validation("Originator is required".to_string()))?;
245 let amount = self
246 .amount
247 .ok_or_else(|| Error::Validation("Amount is required".to_string()))?;
248
249 let transfer = Transfer {
250 transaction_id: self
251 .transaction_id
252 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
253 asset,
254 originator,
255 amount,
256 beneficiary: self.beneficiary,
257 settlement_id: self.settlement_id,
258 memo: self.memo,
259 agents: self.agents,
260 metadata: self.metadata,
261 };
262
263 transfer.validate()?;
265
266 Ok(transfer)
267 }
268}
269
270impl Connectable for Transfer {
271 fn with_connection(&mut self, connect_id: &str) -> &mut Self {
272 self.metadata.insert(
274 "connect_id".to_string(),
275 serde_json::Value::String(connect_id.to_string()),
276 );
277 self
278 }
279
280 fn has_connection(&self) -> bool {
281 self.metadata.contains_key("connect_id")
282 }
283
284 fn connection_id(&self) -> Option<&str> {
285 self.metadata.get("connect_id").and_then(|v| v.as_str())
286 }
287}
288
289impl TapMessageBody for Transfer {
290 fn message_type() -> &'static str {
291 "https://tap.rsvp/schema/1.0#transfer"
292 }
293
294 fn validate(&self) -> Result<()> {
295 if self.asset.namespace().is_empty() || self.asset.reference().is_empty() {
297 return Err(Error::Validation("Asset ID is invalid".to_string()));
298 }
299
300 if self.originator.id.is_empty() {
302 return Err(Error::Validation("Originator ID is required".to_string()));
303 }
304
305 if self.amount.is_empty() {
307 return Err(Error::Validation("Amount is required".to_string()));
308 }
309
310 match self.amount.parse::<f64>() {
312 Ok(amount) if amount <= 0.0 => {
313 return Err(Error::Validation("Amount must be positive".to_string()));
314 }
315 Err(_) => {
316 return Err(Error::Validation(
317 "Amount must be a valid number".to_string(),
318 ));
319 }
320 _ => {}
321 }
322
323 Ok(())
324 }
325
326 fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
327 let mut body_json =
329 serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
330
331 if let Some(body_obj) = body_json.as_object_mut() {
333 body_obj.insert(
334 "@type".to_string(),
335 serde_json::Value::String(Self::message_type().to_string()),
336 );
337 }
338
339 let mut agent_dids = Vec::new();
341
342 agent_dids.push(self.originator.id.clone());
344
345 if let Some(beneficiary) = &self.beneficiary {
347 agent_dids.push(beneficiary.id.clone());
348 }
349
350 for agent in &self.agents {
352 agent_dids.push(agent.id.clone());
353 }
354
355 agent_dids.sort();
357 agent_dids.dedup();
358
359 agent_dids.retain(|did| did != from);
361
362 let now = Utc::now().timestamp() as u64;
363
364 let pthid = self
366 .connection_id()
367 .map(|connect_id| connect_id.to_string());
368
369 let message = PlainMessage {
371 id: uuid::Uuid::new_v4().to_string(),
372 typ: "application/didcomm-plain+json".to_string(),
373 type_: Self::message_type().to_string(),
374 body: body_json,
375 from: from.to_string(),
376 to: agent_dids,
377 thid: None,
378 pthid,
379 created_time: Some(now),
380 expires_time: None,
381 extra_headers: std::collections::HashMap::new(),
382 from_prior: None,
383 attachments: None,
384 };
385
386 Ok(message)
387 }
388
389 fn to_didcomm_with_route<'a, I>(&self, from: &str, to: I) -> Result<PlainMessage>
390 where
391 I: IntoIterator<Item = &'a str>,
392 {
393 let mut message = self.to_didcomm(from)?;
395
396 let to_vec: Vec<String> = to.into_iter().map(String::from).collect();
398 if !to_vec.is_empty() {
399 message.to = to_vec;
400 }
401
402 if let Some(connect_id) = self.connection_id() {
404 message.pthid = Some(connect_id.to_string());
405 }
406
407 Ok(message)
408 }
409}
410
411impl Authorizable for Transfer {
412 fn authorize(&self, note: Option<String>) -> Authorize {
413 Authorize {
414 transaction_id: self.transaction_id.clone(),
415 note,
416 }
417 }
418
419 fn update_policies(&self, transaction_id: String, policies: Vec<Policy>) -> UpdatePolicies {
420 UpdatePolicies {
421 transaction_id,
422 policies,
423 }
424 }
425
426 fn replace_agent(
427 &self,
428 transaction_id: String,
429 original_agent: String,
430 replacement: Participant,
431 ) -> ReplaceAgent {
432 ReplaceAgent {
433 transaction_id,
434 original: original_agent,
435 replacement,
436 }
437 }
438
439 fn remove_agent(&self, transaction_id: String, agent: String) -> RemoveAgent {
440 RemoveAgent {
441 transaction_id,
442 agent,
443 }
444 }
445}
446
447impl_tap_message!(Transfer);