qudag_exchange_core/
transaction.rs

1//! Transaction handling for QuDAG Exchange
2//!
3//! Implements transactions with quantum-resistant signatures using QuDAG's crypto primitives
4
5#[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/// Transaction identifier
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct TransactionId(Hash);
18
19impl TransactionId {
20    /// Create from hash
21    pub fn from_hash(hash: Hash) -> Self {
22        Self(hash)
23    }
24
25    /// Get the underlying hash
26    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/// Transaction status in the system
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40pub enum TransactionStatus {
41    /// Transaction is pending confirmation
42    Pending,
43    /// Transaction is being processed
44    Processing,
45    /// Transaction is confirmed and finalized
46    Confirmed,
47    /// Transaction was rejected
48    Rejected,
49    /// Transaction expired before confirmation
50    Expired,
51}
52
53/// Transaction type
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub enum TransactionType {
56    /// Simple transfer of rUv between accounts
57    Transfer {
58        /// Source account
59        from: AccountId,
60        /// Destination account
61        to: AccountId,
62        /// Amount to transfer
63        amount: rUv,
64    },
65    /// Mint new rUv tokens (privileged operation)
66    Mint {
67        /// Account to receive new tokens
68        to: AccountId,
69        /// Amount to mint
70        amount: rUv,
71    },
72    /// Burn rUv tokens
73    Burn {
74        /// Account to burn from
75        from: AccountId,
76        /// Amount to burn
77        amount: rUv,
78    },
79    /// Create a new account
80    CreateAccount {
81        /// New account ID
82        account: AccountId,
83        /// Initial balance (if any)
84        initial_balance: rUv,
85    },
86    /// Update account metadata
87    UpdateAccount {
88        /// Account to update
89        account: AccountId,
90        /// New public key (optional)
91        public_key: Option<Vec<u8>>,
92    },
93}
94
95/// Main transaction structure
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct Transaction {
98    /// Transaction type and data
99    pub tx_type: TransactionType,
100
101    /// Transaction nonce (for ordering and replay protection)
102    pub nonce: Nonce,
103
104    /// Timestamp when transaction was created
105    pub timestamp: Timestamp,
106
107    /// Fee paid for the transaction (in rUv)
108    pub fee: rUv,
109
110    /// Optional expiry timestamp
111    pub expires_at: Option<Timestamp>,
112
113    /// Quantum-resistant signature
114    pub signature: Option<TransactionSignature>,
115
116    /// Additional metadata
117    pub metadata: TransactionMetadata,
118}
119
120/// Transaction signature using quantum-resistant algorithms
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct TransactionSignature {
123    /// Algorithm used (e.g., "ML-DSA-87", "ML-DSA-65")
124    pub algorithm: String,
125
126    /// Public key of the signer
127    pub public_key: Vec<u8>,
128
129    /// The signature bytes
130    pub signature: Vec<u8>,
131}
132
133/// Transaction metadata
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct TransactionMetadata {
136    /// Optional memo/note
137    pub memo: Option<String>,
138
139    /// Protocol version
140    pub version: u32,
141
142    /// Chain ID for replay protection
143    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    /// Create a new transfer transaction
158    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    /// Create a new mint transaction
171    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    /// Create a new burn transaction
184    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    /// Set expiry timestamp
197    pub fn with_expiry(mut self, expires_at: Timestamp) -> Self {
198        self.expires_at = Some(expires_at);
199        self
200    }
201
202    /// Set memo
203    pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
204        self.metadata.memo = Some(memo.into());
205        self
206    }
207
208    /// Compute transaction ID
209    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    /// Serialize transaction to bytes for signing/hashing
216    pub fn to_bytes(&self) -> Result<Vec<u8>> {
217        // Create a version without signature for signing
218        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    /// Get the sender account (if applicable)
225    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    /// Get the total cost (amount + fee)
235    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    /// Check if transaction is expired
248    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    /// Sign the transaction using QuDAG crypto
255    #[cfg(feature = "std")]
256    pub fn sign(&mut self, keypair: &qudag_crypto::MlDsaKeyPair) -> Result<()> {
257        let message = self.to_bytes()?;
258
259        // Sign the message
260        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    /// Verify the transaction signature
278    #[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        // Create public key from bytes
288        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        // Verify the signature
292        match public_key.verify(&message, &sig_data.signature) {
293            Ok(()) => Ok(true),
294            Err(_) => Ok(false),
295        }
296    }
297
298    /// Get current timestamp (platform-specific)
299    fn current_timestamp() -> Timestamp {
300        #[cfg(feature = "std")]
301        {
302            Timestamp::now()
303        }
304        #[cfg(not(feature = "std"))]
305        {
306            // In no_std, timestamp must be provided externally
307            Timestamp::new(0)
308        }
309    }
310}
311
312/// Builder for constructing transactions
313pub 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    /// Create a new transaction builder
324    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    /// Set transaction type
336    pub fn with_type(mut self, tx_type: TransactionType) -> Self {
337        self.tx_type = Some(tx_type);
338        self
339    }
340
341    /// Set nonce
342    pub fn with_nonce(mut self, nonce: Nonce) -> Self {
343        self.nonce = Some(nonce);
344        self
345    }
346
347    /// Set fee
348    pub fn with_fee(mut self, fee: rUv) -> Self {
349        self.fee = fee;
350        self
351    }
352
353    /// Set expiry
354    pub fn with_expiry(mut self, expires_at: Timestamp) -> Self {
355        self.expires_at = Some(expires_at);
356        self
357    }
358
359    /// Set memo
360    pub fn with_memo(mut self, memo: impl Into<String>) -> Self {
361        self.memo = Some(memo.into());
362        self
363    }
364
365    /// Set chain ID
366    pub fn with_chain_id(mut self, chain_id: u64) -> Self {
367        self.chain_id = chain_id;
368        self
369    }
370
371    /// Build the transaction
372    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        // Test mint (no amount cost)
470        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        // Same transactions should have same ID
487        assert_eq!(tx1.id().unwrap(), tx2.id().unwrap());
488
489        // Different nonce should give different ID
490        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        // No expiry by default
506        assert!(!tx.is_expired(Timestamp::new(u64::MAX)));
507
508        // Set expiry
509        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}