1#[cfg(not(feature = "std"))]
6use alloc::{string::String, vec::Vec};
7
8use crate::{
9 account::AccountId,
10 types::{rUv, Hash, Nonce, Timestamp},
11 Error, Result,
12};
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct TransactionId(Hash);
18
19impl TransactionId {
20 pub fn from_hash(hash: Hash) -> Self {
22 Self(hash)
23 }
24
25 pub fn hash(&self) -> &Hash {
27 &self.0
28 }
29}
30
31#[cfg(feature = "std")]
32impl std::fmt::Display for TransactionId {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(f, "tx_{}", self.0)
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum TransactionStatus {
41 Pending,
43 Processing,
45 Confirmed,
47 Rejected,
49 Expired,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub enum TransactionType {
56 Transfer {
58 from: AccountId,
60 to: AccountId,
62 amount: rUv,
64 },
65 Mint {
67 to: AccountId,
69 amount: rUv,
71 },
72 Burn {
74 from: AccountId,
76 amount: rUv,
78 },
79 CreateAccount {
81 account: AccountId,
83 initial_balance: rUv,
85 },
86 UpdateAccount {
88 account: AccountId,
90 public_key: Option<Vec<u8>>,
92 },
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct Transaction {
98 pub tx_type: TransactionType,
100
101 pub nonce: Nonce,
103
104 pub timestamp: Timestamp,
106
107 pub fee: rUv,
109
110 pub expires_at: Option<Timestamp>,
112
113 pub signature: Option<TransactionSignature>,
115
116 pub metadata: TransactionMetadata,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct TransactionSignature {
123 pub algorithm: String,
125
126 pub public_key: Vec<u8>,
128
129 pub signature: Vec<u8>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct TransactionMetadata {
136 pub memo: Option<String>,
138
139 pub version: u32,
141
142 pub chain_id: u64,
144}
145
146impl Default for TransactionMetadata {
147 fn default() -> Self {
148 Self {
149 memo: None,
150 version: 1,
151 chain_id: 1,
152 }
153 }
154}
155
156impl Transaction {
157 pub fn transfer(from: AccountId, to: AccountId, amount: rUv, nonce: Nonce, fee: rUv) -> Self {
159 Self {
160 tx_type: TransactionType::Transfer { from, to, amount },
161 nonce,
162 timestamp: Self::current_timestamp(),
163 fee,
164 expires_at: None,
165 signature: None,
166 metadata: TransactionMetadata::default(),
167 }
168 }
169
170 pub fn mint(to: AccountId, amount: rUv, nonce: Nonce, fee: rUv) -> Self {
172 Self {
173 tx_type: TransactionType::Mint { to, amount },
174 nonce,
175 timestamp: Self::current_timestamp(),
176 fee,
177 expires_at: None,
178 signature: None,
179 metadata: TransactionMetadata::default(),
180 }
181 }
182
183 pub fn burn(from: AccountId, amount: rUv, nonce: Nonce, fee: rUv) -> Self {
185 Self {
186 tx_type: TransactionType::Burn { from, amount },
187 nonce,
188 timestamp: Self::current_timestamp(),
189 fee,
190 expires_at: None,
191 signature: None,
192 metadata: TransactionMetadata::default(),
193 }
194 }
195
196 pub fn with_expiry(mut self, expires_at: Timestamp) -> Self {
198 self.expires_at = Some(expires_at);
199 self
200 }
201
202 pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
204 self.metadata.memo = Some(memo.into());
205 self
206 }
207
208 pub fn id(&self) -> Result<TransactionId> {
210 let bytes = self.to_bytes()?;
211 let hash = blake3::hash(&bytes);
212 Ok(TransactionId::from_hash(Hash::from_bytes(*hash.as_bytes())))
213 }
214
215 pub fn to_bytes(&self) -> Result<Vec<u8>> {
217 let mut tx_for_signing = self.clone();
219 tx_for_signing.signature = None;
220
221 bincode::serialize(&tx_for_signing).map_err(|e| Error::SerializationError(e.to_string()))
222 }
223
224 pub fn sender(&self) -> Option<&AccountId> {
226 match &self.tx_type {
227 TransactionType::Transfer { from, .. } => Some(from),
228 TransactionType::Burn { from, .. } => Some(from),
229 TransactionType::UpdateAccount { account, .. } => Some(account),
230 _ => None,
231 }
232 }
233
234 pub fn total_cost(&self) -> Result<rUv> {
236 let amount = match &self.tx_type {
237 TransactionType::Transfer { amount, .. } => *amount,
238 TransactionType::Burn { amount, .. } => *amount,
239 _ => rUv::ZERO,
240 };
241
242 amount
243 .checked_add(self.fee)
244 .ok_or_else(|| Error::Other("Transaction cost overflow".into()))
245 }
246
247 pub fn is_expired(&self, current_time: Timestamp) -> bool {
249 self.expires_at
250 .map(|exp| current_time > exp)
251 .unwrap_or(false)
252 }
253
254 #[cfg(feature = "std")]
256 pub fn sign(&mut self, keypair: &qudag_crypto::MlDsaKeyPair) -> Result<()> {
257 let message = self.to_bytes()?;
258
259 let signature = keypair
261 .sign(&message, &mut rand::thread_rng())
262 .map_err(|e| Error::Other(format!("Signing failed: {:?}", e)))?;
263
264 let public_key = keypair
265 .to_public_key()
266 .map_err(|e| Error::Other(format!("Public key extraction failed: {:?}", e)))?;
267
268 self.signature = Some(TransactionSignature {
269 algorithm: "ML-DSA-87".to_string(),
270 public_key: public_key.as_bytes().to_vec(),
271 signature,
272 });
273
274 Ok(())
275 }
276
277 #[cfg(feature = "std")]
279 pub fn verify_signature(&self) -> Result<bool> {
280 let sig_data = self
281 .signature
282 .as_ref()
283 .ok_or_else(|| Error::Other("No signature present".into()))?;
284
285 let message = self.to_bytes()?;
286
287 let public_key = qudag_crypto::MlDsaPublicKey::from_bytes(&sig_data.public_key)
289 .map_err(|e| Error::Other(format!("Invalid public key: {:?}", e)))?;
290
291 match public_key.verify(&message, &sig_data.signature) {
293 Ok(()) => Ok(true),
294 Err(_) => Ok(false),
295 }
296 }
297
298 fn current_timestamp() -> Timestamp {
300 #[cfg(feature = "std")]
301 {
302 Timestamp::now()
303 }
304 #[cfg(not(feature = "std"))]
305 {
306 Timestamp::new(0)
308 }
309 }
310}
311
312pub struct TransactionBuilder {
314 tx_type: Option<TransactionType>,
315 nonce: Option<Nonce>,
316 fee: rUv,
317 expires_at: Option<Timestamp>,
318 memo: Option<String>,
319 chain_id: u64,
320}
321
322impl TransactionBuilder {
323 pub fn new() -> Self {
325 Self {
326 tx_type: None,
327 nonce: None,
328 fee: rUv::ZERO,
329 expires_at: None,
330 memo: None,
331 chain_id: 1,
332 }
333 }
334
335 pub fn with_type(mut self, tx_type: TransactionType) -> Self {
337 self.tx_type = Some(tx_type);
338 self
339 }
340
341 pub fn with_nonce(mut self, nonce: Nonce) -> Self {
343 self.nonce = Some(nonce);
344 self
345 }
346
347 pub fn with_fee(mut self, fee: rUv) -> Self {
349 self.fee = fee;
350 self
351 }
352
353 pub fn with_expiry(mut self, expires_at: Timestamp) -> Self {
355 self.expires_at = Some(expires_at);
356 self
357 }
358
359 pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
361 self.memo = Some(memo.into());
362 self
363 }
364
365 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
367 self.chain_id = chain_id;
368 self
369 }
370
371 pub fn build(self) -> Result<Transaction> {
373 let tx_type = self
374 .tx_type
375 .ok_or_else(|| Error::Other("Transaction type not set".into()))?;
376 let nonce = self
377 .nonce
378 .ok_or_else(|| Error::Other("Nonce not set".into()))?;
379
380 Ok(Transaction {
381 tx_type,
382 nonce,
383 timestamp: Transaction::current_timestamp(),
384 fee: self.fee,
385 expires_at: self.expires_at,
386 signature: None,
387 metadata: TransactionMetadata {
388 memo: self.memo,
389 version: 1,
390 chain_id: self.chain_id,
391 },
392 })
393 }
394}
395
396impl Default for TransactionBuilder {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_transaction_creation() {
408 let from = AccountId::new("alice");
409 let to = AccountId::new("bob");
410 let amount = rUv::new(100);
411 let fee = rUv::new(1);
412 let nonce = Nonce::new(1);
413
414 let tx = Transaction::transfer(from.clone(), to.clone(), amount, nonce, fee);
415
416 match &tx.tx_type {
417 TransactionType::Transfer {
418 from: f,
419 to: t,
420 amount: a,
421 } => {
422 assert_eq!(f, &from);
423 assert_eq!(t, &to);
424 assert_eq!(*a, amount);
425 }
426 _ => panic!("Wrong transaction type"),
427 }
428
429 assert_eq!(tx.nonce, nonce);
430 assert_eq!(tx.fee, fee);
431 assert!(tx.signature.is_none());
432 }
433
434 #[test]
435 fn test_transaction_builder() {
436 let to = AccountId::new("charlie");
437 let amount = rUv::new(500);
438 let fee = rUv::new(5);
439 let nonce = Nonce::new(42);
440
441 let tx = TransactionBuilder::new()
442 .with_type(TransactionType::Mint {
443 to: to.clone(),
444 amount,
445 })
446 .with_nonce(nonce)
447 .with_fee(fee)
448 .with_memo("Test mint")
449 .with_chain_id(42)
450 .build()
451 .unwrap();
452
453 assert_eq!(tx.fee, fee);
454 assert_eq!(tx.nonce, nonce);
455 assert_eq!(tx.metadata.memo.as_deref(), Some("Test mint"));
456 assert_eq!(tx.metadata.chain_id, 42);
457 }
458
459 #[test]
460 fn test_transaction_cost() {
461 let from = AccountId::new("alice");
462 let to = AccountId::new("bob");
463 let amount = rUv::new(100);
464 let fee = rUv::new(10);
465
466 let tx = Transaction::transfer(from, to, amount, Nonce::new(1), fee);
467 assert_eq!(tx.total_cost().unwrap(), rUv::new(110));
468
469 let mint_tx = Transaction::mint(AccountId::new("charlie"), amount, Nonce::new(1), fee);
471 assert_eq!(mint_tx.total_cost().unwrap(), fee);
472 }
473
474 #[test]
475 fn test_transaction_id() {
476 let tx1 = Transaction::transfer(
477 AccountId::new("alice"),
478 AccountId::new("bob"),
479 rUv::new(100),
480 Nonce::new(1),
481 rUv::new(1),
482 );
483
484 let tx2 = tx1.clone();
485
486 assert_eq!(tx1.id().unwrap(), tx2.id().unwrap());
488
489 let mut tx3 = tx1.clone();
491 tx3.nonce = Nonce::new(2);
492 assert_ne!(tx1.id().unwrap(), tx3.id().unwrap());
493 }
494
495 #[test]
496 fn test_transaction_expiry() {
497 let mut tx = Transaction::transfer(
498 AccountId::new("alice"),
499 AccountId::new("bob"),
500 rUv::new(100),
501 Nonce::new(1),
502 rUv::new(1),
503 );
504
505 assert!(!tx.is_expired(Timestamp::new(u64::MAX)));
507
508 tx.expires_at = Some(Timestamp::new(1000));
510 assert!(!tx.is_expired(Timestamp::new(999)));
511 assert!(!tx.is_expired(Timestamp::new(1000)));
512 assert!(tx.is_expired(Timestamp::new(1001)));
513 }
514}