Skip to main content

sumchain_primitives/
transaction.rs

1//! Transaction types for SUM Chain.
2//!
3//! Transactions represent state changes on the blockchain:
4//! - Native token transfers (Koppa)
5//! - NFT operations (SUM-721)
6//!
7//! Each transaction must be signed by the sender's private key.
8
9use serde::{Deserialize, Serialize};
10use serde_big_array::BigArray;
11
12use crate::agreement::AgreementTxData;
13use crate::docclass::DocClassTxData;
14use crate::employment::EmploymentTxData;
15use crate::equity::EquityTxData;
16use crate::finance::FinanceTxData;
17use crate::healthcare::HealthcareTxData;
18use crate::legal::LegalTxData;
19use crate::messaging::MessagingTxData;
20use crate::policy_account::PolicyAccountTxData;
21use crate::property::PropertyTxData;
22use crate::staking::StakingTxData;
23use crate::tax::TaxTxData;
24use crate::{Address, Balance, ChainId, Hash, Nonce};
25
26/// Transaction type identifier
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[repr(u8)]
29pub enum TxType {
30    /// Native token transfer
31    Transfer = 0,
32    /// NFT operation (SUM-721)
33    Nft = 1,
34    /// Token operation (SRC-20)
35    Token = 2,
36    /// Smart contract deployment
37    ContractDeploy = 3,
38    /// Smart contract call
39    ContractCall = 4,
40    /// Staking operation
41    Staking = 5,
42    /// Messaging operation (SRC-201)
43    Messaging = 6,
44    /// DocClass operation (SRC-80X/81X)
45    DocClass = 7,
46    /// Tax & Compliance operation (SRC-82X)
47    Tax = 8,
48    /// Business, Governance & Equity operation (SRC-83X)
49    Equity = 9,
50    /// Agreement & IP operation (SRC-84X)
51    Agreement = 10,
52    /// Legal Process operation (SRC-85X)
53    Legal = 11,
54    /// Property & Insurance operation (SRC-86X)
55    Property = 12,
56    /// Healthcare & Membership operation (SRC-87X)
57    Healthcare = 13,
58    /// Employment & HR operation (SRC-88X)
59    Employment = 14,
60    /// Finance & Banking operation (SRC-89X)
61    Finance = 15,
62    /// Policy Account operation (Group governance)
63    PolicyAccount = 16,
64    /// Node Registry operation (register/manage network nodes)
65    NodeRegistry = 17,
66    /// Storage Metadata operation (file registration, ACL, fee pool)
67    StorageMetadata = 18,
68    /// V2 Node Registry operation (e.g. encryption-key registration — SNIP V2 Ask 3)
69    NodeRegistryV2 = 19,
70    /// V2 Storage Metadata operation (Pending lifecycle, bundle storage, abandonment — SNIP V2 Phase 1)
71    StorageMetadataV2 = 20,
72    /// OmniNode inference attestation (Stage 6 handoff) — variant index frozen at 21,
73    /// append-only; see crates/primitives/src/inference_attestation.rs for wire shape.
74    InferenceAttestation = 21,
75    /// SRC-817/818 Education-LMS suite — variant index frozen at 22,
76    /// append-only; see crates/primitives/src/education.rs for wire shape.
77    Education = 22,
78}
79
80impl TxType {
81    /// Convert from byte
82    pub fn from_byte(b: u8) -> Option<Self> {
83        match b {
84            0 => Some(TxType::Transfer),
85            1 => Some(TxType::Nft),
86            2 => Some(TxType::Token),
87            3 => Some(TxType::ContractDeploy),
88            4 => Some(TxType::ContractCall),
89            5 => Some(TxType::Staking),
90            6 => Some(TxType::Messaging),
91            7 => Some(TxType::DocClass),
92            8 => Some(TxType::Tax),
93            9 => Some(TxType::Equity),
94            10 => Some(TxType::Agreement),
95            11 => Some(TxType::Legal),
96            12 => Some(TxType::Property),
97            13 => Some(TxType::Healthcare),
98            14 => Some(TxType::Employment),
99            15 => Some(TxType::Finance),
100            16 => Some(TxType::PolicyAccount),
101            17 => Some(TxType::NodeRegistry),
102            18 => Some(TxType::StorageMetadata),
103            19 => Some(TxType::NodeRegistryV2),
104            20 => Some(TxType::StorageMetadataV2),
105            21 => Some(TxType::InferenceAttestation),
106            22 => Some(TxType::Education),
107            _ => None,
108        }
109    }
110}
111
112/// Unsigned transaction data (legacy transfer format for backwards compatibility)
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct Transaction {
115    /// Chain ID to prevent replay across networks
116    pub chain_id: ChainId,
117    /// Sender address
118    pub from: Address,
119    /// Recipient address
120    pub to: Address,
121    /// Amount to transfer (in smallest unit)
122    pub amount: Balance,
123    /// Transaction fee paid to validator
124    pub fee: Balance,
125    /// Sender's nonce (must match account nonce)
126    pub nonce: Nonce,
127}
128
129/// NFT-specific transaction data
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct NftTxData {
132    /// Collection ID (32 bytes)
133    pub collection_id: [u8; 32],
134    /// Token ID (0 for collection-level operations)
135    pub token_id: u64,
136    /// NFT operation code
137    pub operation: NftOperation,
138    /// Operation-specific data (serialized)
139    pub data: Vec<u8>,
140}
141
142/// NFT operation codes
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144#[repr(u8)]
145pub enum NftOperation {
146    /// Create a new collection
147    CreateCollection = 0,
148    /// Mint a new token
149    Mint = 1,
150    /// Mint a certified document
151    MintDocument = 2,
152    /// Batch mint tokens
153    BatchMint = 3,
154    /// Transfer a token
155    Transfer = 4,
156    /// Approve an address for a token
157    Approve = 5,
158    /// Set approval for all tokens
159    SetApprovalForAll = 6,
160    /// Burn a token
161    Burn = 7,
162    /// Update token metadata
163    UpdateMetadata = 8,
164    /// Transfer collection ownership
165    TransferCollectionOwnership = 9,
166    /// Update collection config
167    UpdateCollectionConfig = 10,
168    /// Lock a token
169    LockToken = 11,
170    /// Unlock a token
171    UnlockToken = 12,
172}
173
174/// SRC-20 Token operation codes
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176#[repr(u8)]
177pub enum TokenOperation {
178    /// Create a new token
179    Create = 0,
180    /// Mint new tokens
181    Mint = 1,
182    /// Burn tokens
183    Burn = 2,
184    /// Transfer tokens
185    Transfer = 3,
186    /// Approve spending allowance
187    Approve = 4,
188    /// Transfer using allowance
189    TransferFrom = 5,
190    /// Pause token transfers
191    Pause = 6,
192    /// Unpause token transfers
193    Unpause = 7,
194    /// Transfer token ownership
195    TransferOwnership = 8,
196    /// Add a minter
197    AddMinter = 9,
198    /// Remove a minter
199    RemoveMinter = 10,
200}
201
202impl TokenOperation {
203    /// Convert from byte
204    pub fn from_byte(b: u8) -> Option<Self> {
205        match b {
206            0 => Some(TokenOperation::Create),
207            1 => Some(TokenOperation::Mint),
208            2 => Some(TokenOperation::Burn),
209            3 => Some(TokenOperation::Transfer),
210            4 => Some(TokenOperation::Approve),
211            5 => Some(TokenOperation::TransferFrom),
212            6 => Some(TokenOperation::Pause),
213            7 => Some(TokenOperation::Unpause),
214            8 => Some(TokenOperation::TransferOwnership),
215            9 => Some(TokenOperation::AddMinter),
216            10 => Some(TokenOperation::RemoveMinter),
217            _ => None,
218        }
219    }
220
221    /// Check if this operation requires token ownership
222    pub fn requires_ownership(&self) -> bool {
223        matches!(
224            self,
225            TokenOperation::Pause
226                | TokenOperation::Unpause
227                | TokenOperation::TransferOwnership
228                | TokenOperation::AddMinter
229                | TokenOperation::RemoveMinter
230        )
231    }
232
233    /// Check if this operation requires minter role
234    pub fn requires_minter(&self) -> bool {
235        matches!(self, TokenOperation::Mint)
236    }
237}
238
239/// SRC-20 Token-specific transaction data
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct TokenTxData {
242    /// Token ID (32 bytes) - zero for Create operation
243    pub token_id: [u8; 32],
244    /// Token operation code
245    pub operation: TokenOperation,
246    /// Operation-specific data (serialized)
247    pub data: Vec<u8>,
248}
249
250/// Smart contract deployment data
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct ContractDeployData {
253    /// WASM bytecode
254    pub code: Vec<u8>,
255    /// Init method name (usually "new" or "init")
256    pub init_method: String,
257    /// Init method arguments (serialized)
258    pub init_args: Vec<u8>,
259    /// Initial Koppa to send to contract
260    pub value: Balance,
261    /// Gas limit for deployment
262    pub gas_limit: u64,
263}
264
265/// Smart contract call data
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct ContractCallData {
268    /// Contract address to call
269    pub contract: Address,
270    /// Method name to call
271    pub method: String,
272    /// Method arguments (serialized)
273    pub args: Vec<u8>,
274    /// Koppa to send with call
275    pub value: Balance,
276    /// Gas limit for call
277    pub gas_limit: u64,
278}
279
280impl NftOperation {
281    /// Convert from byte
282    pub fn from_byte(b: u8) -> Option<Self> {
283        match b {
284            0 => Some(NftOperation::CreateCollection),
285            1 => Some(NftOperation::Mint),
286            2 => Some(NftOperation::MintDocument),
287            3 => Some(NftOperation::BatchMint),
288            4 => Some(NftOperation::Transfer),
289            5 => Some(NftOperation::Approve),
290            6 => Some(NftOperation::SetApprovalForAll),
291            7 => Some(NftOperation::Burn),
292            8 => Some(NftOperation::UpdateMetadata),
293            9 => Some(NftOperation::TransferCollectionOwnership),
294            10 => Some(NftOperation::UpdateCollectionConfig),
295            11 => Some(NftOperation::LockToken),
296            12 => Some(NftOperation::UnlockToken),
297            _ => None,
298        }
299    }
300
301    /// Check if this operation creates a new collection
302    pub fn is_collection_creation(&self) -> bool {
303        matches!(self, NftOperation::CreateCollection)
304    }
305
306    /// Check if this operation requires token ownership
307    pub fn requires_token_ownership(&self) -> bool {
308        matches!(
309            self,
310            NftOperation::Transfer
311                | NftOperation::Approve
312                | NftOperation::Burn
313                | NftOperation::UpdateMetadata
314                | NftOperation::LockToken
315                | NftOperation::UnlockToken
316        )
317    }
318
319    /// Check if this operation requires collection ownership
320    pub fn requires_collection_ownership(&self) -> bool {
321        matches!(
322            self,
323            NftOperation::Mint
324                | NftOperation::MintDocument
325                | NftOperation::BatchMint
326                | NftOperation::TransferCollectionOwnership
327                | NftOperation::UpdateCollectionConfig
328        )
329    }
330}
331
332/// Extended transaction with payload type
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334pub struct TransactionV2 {
335    /// Chain ID to prevent replay across networks
336    pub chain_id: ChainId,
337    /// Sender address
338    pub from: Address,
339    /// Transaction fee paid to validator
340    pub fee: Balance,
341    /// Sender's nonce (must match account nonce)
342    pub nonce: Nonce,
343    /// Transaction payload
344    pub payload: TxPayload,
345}
346
347/// Transaction payload - transfer, NFT, Token, Contract, Staking, or Messaging operation
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349pub enum TxPayload {
350    /// Native token transfer
351    Transfer {
352        /// Recipient address
353        to: Address,
354        /// Amount to transfer
355        amount: Balance,
356    },
357    /// NFT operation (SUM-721)
358    Nft(NftTxData),
359    /// Token operation (SRC-20)
360    Token(TokenTxData),
361    /// Smart contract deployment
362    ContractDeploy(ContractDeployData),
363    /// Smart contract call
364    ContractCall(ContractCallData),
365    /// Staking operation
366    Staking(StakingTxData),
367    /// Messaging operation (SRC-201)
368    Messaging(MessagingTxData),
369    /// DocClass operation (SRC-80X/81X)
370    DocClass(DocClassTxData),
371    /// Tax & Compliance operation (SRC-82X)
372    Tax(TaxTxData),
373    /// Business, Governance & Equity operation (SRC-83X)
374    Equity(EquityTxData),
375    /// Agreement & IP operation (SRC-84X)
376    Agreement(AgreementTxData),
377    /// Legal Process operation (SRC-85X)
378    Legal(LegalTxData),
379    /// Property & Insurance operation (SRC-86X)
380    Property(PropertyTxData),
381    /// Healthcare & Membership operation (SRC-87X)
382    Healthcare(HealthcareTxData),
383    /// Employment & HR operation (SRC-88X)
384    Employment(EmploymentTxData),
385    /// Finance & Banking operation (SRC-89X)
386    Finance(FinanceTxData),
387    /// Policy Account operation (Group governance)
388    PolicyAccount(PolicyAccountTxData),
389    /// Node Registry operation (register/manage network nodes)
390    NodeRegistry(crate::node_registry::NodeRegistryTxData),
391    /// Storage Metadata operation (file registration, ACL, fee pool)
392    StorageMetadata(crate::storage_metadata::StorageMetadataTxData),
393    /// V2 Node Registry operation (e.g. RegisterEncryptionKey — SNIP V2 Ask 3)
394    NodeRegistryV2(crate::node_registry::NodeRegistryV2TxData),
395    /// V2 Storage Metadata operation (RegisterFilePendingV2, AbandonFileV2, etc. — SNIP V2 Phase 1)
396    StorageMetadataV2(crate::storage_metadata::StorageMetadataV2TxData),
397    /// OmniNode inference attestation (Stage 6 handoff). Variant index 21 in
398    /// `TxType`; bincode-serialized as the 22nd enum variant in `TxPayload`.
399    /// **Append-only**: never reorder above this, or every existing
400    /// serialized tx re-decodes as a different operation. See
401    /// crates/primitives/src/inference_attestation.rs and the wire fixtures
402    /// in crates/primitives/tests/inference_attestation_fixtures.rs.
403    InferenceAttestation(crate::inference_attestation::InferenceAttestationTxData),
404    /// SRC-817/818 Education-LMS suite. `TxType` discriminant 22;
405    /// bincode-serialized as the 23rd enum variant in `TxPayload`
406    /// (declaration ordinal 22). **Append-only**: never reorder above
407    /// this. See crates/primitives/src/education.rs and the wire
408    /// fixtures in crates/primitives/tests/education_fixtures.rs.
409    Education(crate::education::EducationTxData),
410}
411
412impl TransactionV2 {
413    /// Create a new transfer transaction
414    pub fn transfer(
415        chain_id: ChainId,
416        from: Address,
417        to: Address,
418        amount: Balance,
419        fee: Balance,
420        nonce: Nonce,
421    ) -> Self {
422        Self {
423            chain_id,
424            from,
425            fee,
426            nonce,
427            payload: TxPayload::Transfer { to, amount },
428        }
429    }
430
431    /// Create a new NFT transaction
432    pub fn nft(
433        chain_id: ChainId,
434        from: Address,
435        fee: Balance,
436        nonce: Nonce,
437        nft_data: NftTxData,
438    ) -> Self {
439        Self {
440            chain_id,
441            from,
442            fee,
443            nonce,
444            payload: TxPayload::Nft(nft_data),
445        }
446    }
447
448    /// Create a new Token (SRC-20) transaction
449    pub fn token(
450        chain_id: ChainId,
451        from: Address,
452        fee: Balance,
453        nonce: Nonce,
454        token_data: TokenTxData,
455    ) -> Self {
456        Self {
457            chain_id,
458            from,
459            fee,
460            nonce,
461            payload: TxPayload::Token(token_data),
462        }
463    }
464
465    /// Create a new contract deployment transaction
466    pub fn contract_deploy(
467        chain_id: ChainId,
468        from: Address,
469        fee: Balance,
470        nonce: Nonce,
471        deploy_data: ContractDeployData,
472    ) -> Self {
473        Self {
474            chain_id,
475            from,
476            fee,
477            nonce,
478            payload: TxPayload::ContractDeploy(deploy_data),
479        }
480    }
481
482    /// Create a new contract call transaction
483    pub fn contract_call(
484        chain_id: ChainId,
485        from: Address,
486        fee: Balance,
487        nonce: Nonce,
488        call_data: ContractCallData,
489    ) -> Self {
490        Self {
491            chain_id,
492            from,
493            fee,
494            nonce,
495            payload: TxPayload::ContractCall(call_data),
496        }
497    }
498
499    /// Create a new staking transaction
500    pub fn staking(
501        chain_id: ChainId,
502        from: Address,
503        fee: Balance,
504        nonce: Nonce,
505        staking_data: StakingTxData,
506    ) -> Self {
507        Self {
508            chain_id,
509            from,
510            fee,
511            nonce,
512            payload: TxPayload::Staking(staking_data),
513        }
514    }
515
516    /// Create a new messaging transaction
517    pub fn messaging(
518        chain_id: ChainId,
519        from: Address,
520        fee: Balance,
521        nonce: Nonce,
522        messaging_data: MessagingTxData,
523    ) -> Self {
524        Self {
525            chain_id,
526            from,
527            fee,
528            nonce,
529            payload: TxPayload::Messaging(messaging_data),
530        }
531    }
532
533    /// Get the transaction type
534    pub fn tx_type(&self) -> TxType {
535        match &self.payload {
536            TxPayload::Transfer { .. } => TxType::Transfer,
537            TxPayload::Nft(_) => TxType::Nft,
538            TxPayload::Token(_) => TxType::Token,
539            TxPayload::ContractDeploy(_) => TxType::ContractDeploy,
540            TxPayload::ContractCall(_) => TxType::ContractCall,
541            TxPayload::Staking(_) => TxType::Staking,
542            TxPayload::Messaging(_) => TxType::Messaging,
543            TxPayload::DocClass(_) => TxType::DocClass,
544            TxPayload::Tax(_) => TxType::Tax,
545            TxPayload::Equity(_) => TxType::Equity,
546            TxPayload::Agreement(_) => TxType::Agreement,
547            TxPayload::Legal(_) => TxType::Legal,
548            TxPayload::Property(_) => TxType::Property,
549            TxPayload::Healthcare(_) => TxType::Healthcare,
550            TxPayload::Employment(_) => TxType::Employment,
551            TxPayload::Finance(_) => TxType::Finance,
552            TxPayload::PolicyAccount(_) => TxType::PolicyAccount,
553            TxPayload::NodeRegistry(_) => TxType::NodeRegistry,
554            TxPayload::StorageMetadata(_) => TxType::StorageMetadata,
555            TxPayload::NodeRegistryV2(_) => TxType::NodeRegistryV2,
556            TxPayload::StorageMetadataV2(_) => TxType::StorageMetadataV2,
557            TxPayload::InferenceAttestation(_) => TxType::InferenceAttestation,
558            TxPayload::Education(_) => TxType::Education,
559        }
560    }
561
562    /// Compute the signing hash
563    pub fn signing_hash(&self) -> Hash {
564        let bytes = bincode::serialize(self).expect("TransactionV2 serialization should not fail");
565        Hash::hash(&bytes)
566    }
567
568    /// Serialize to bytes
569    pub fn to_bytes(&self) -> Vec<u8> {
570        bincode::serialize(self).expect("TransactionV2 serialization should not fail")
571    }
572
573    /// Deserialize from bytes
574    pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
575        bincode::deserialize(bytes)
576    }
577
578    /// Get recipient address (for transfers) or contract address (for calls)
579    pub fn recipient(&self) -> Option<Address> {
580        match &self.payload {
581            TxPayload::Transfer { to, .. } => Some(*to),
582            TxPayload::ContractCall(data) => Some(data.contract),
583            TxPayload::Nft(_) => None,
584            TxPayload::Token(_) => None,
585            TxPayload::ContractDeploy(_) => None,
586            TxPayload::Staking(_) => None,
587            TxPayload::Messaging(_) => None, // Recipient is encrypted for privacy
588            TxPayload::DocClass(data) => Some(data.recipient),
589            TxPayload::Tax(data) => Some(data.recipient),
590            TxPayload::Equity(data) => Some(data.recipient),
591            TxPayload::Agreement(data) => Some(data.recipient),
592            TxPayload::Legal(data) => Some(data.recipient),
593            TxPayload::Property(data) => Some(data.recipient),
594            TxPayload::Healthcare(data) => Some(data.recipient),
595            TxPayload::Employment(data) => Some(data.recipient),
596            TxPayload::Finance(data) => Some(data.recipient),
597            TxPayload::PolicyAccount(data) => Some(data.recipient),
598            TxPayload::NodeRegistry(_) => None,
599            TxPayload::StorageMetadata(_) => None,
600            TxPayload::NodeRegistryV2(_) => None,
601            TxPayload::StorageMetadataV2(_) => None,
602            TxPayload::InferenceAttestation(_) => None, // attestation, no value/recipient
603            TxPayload::Education(data) => Some(data.recipient), // Address::ZERO for no-target ops
604        }
605    }
606
607    /// Get transfer amount (for transfers) or value (for contract calls)
608    pub fn amount(&self) -> Balance {
609        match &self.payload {
610            TxPayload::Transfer { amount, .. } => *amount,
611            TxPayload::ContractDeploy(data) => data.value,
612            TxPayload::ContractCall(data) => data.value,
613            TxPayload::Nft(_) => 0,
614            TxPayload::Token(_) => 0,
615            TxPayload::Staking(_) => 0,
616            TxPayload::Messaging(_) => 0, // Koppa attachment is inside message data
617            TxPayload::DocClass(_) => 0,   // Stake/fee handled separately
618            TxPayload::Tax(_) => 0,        // Fee-only operations
619            TxPayload::Equity(_) => 0,     // Fee-only operations
620            TxPayload::Agreement(_) => 0,  // Fee-only operations
621            TxPayload::Legal(_) => 0,      // Fee-only operations
622            TxPayload::Property(_) => 0,   // Fee-only operations
623            TxPayload::Healthcare(_) => 0, // Fee-only operations
624            TxPayload::Employment(_) => 0, // Fee-only operations
625            TxPayload::Finance(_) => 0,    // Fee-only operations
626            TxPayload::PolicyAccount(_) => 0, // Fee-only operations
627            TxPayload::NodeRegistry(_) => 0,    // Stake handled in executor
628            TxPayload::StorageMetadata(_) => 0,  // Fee deposit handled in executor
629            TxPayload::NodeRegistryV2(_) => 0,  // Fee-only (e.g. RegisterEncryptionKey)
630            TxPayload::StorageMetadataV2(_) => 0, // fee_deposit handled in executor
631            TxPayload::InferenceAttestation(_) => 0, // attestation tx, no token value
632            TxPayload::Education(_) => 0,            // education ops carry no token value
633        }
634    }
635
636    /// Convert to legacy Transaction format (only for transfers)
637    pub fn to_legacy(&self) -> Option<Transaction> {
638        match &self.payload {
639            TxPayload::Transfer { to, amount } => Some(Transaction {
640                chain_id: self.chain_id,
641                from: self.from,
642                to: *to,
643                amount: *amount,
644                fee: self.fee,
645                nonce: self.nonce,
646            }),
647            _ => None,
648        }
649    }
650
651    /// Get contract deploy data if this is a deploy transaction
652    pub fn deploy_data(&self) -> Option<&ContractDeployData> {
653        match &self.payload {
654            TxPayload::ContractDeploy(data) => Some(data),
655            _ => None,
656        }
657    }
658
659    /// Get contract call data if this is a call transaction
660    pub fn call_data(&self) -> Option<&ContractCallData> {
661        match &self.payload {
662            TxPayload::ContractCall(data) => Some(data),
663            _ => None,
664        }
665    }
666
667    /// Check if this is a contract transaction
668    pub fn is_contract(&self) -> bool {
669        matches!(
670            self.payload,
671            TxPayload::ContractDeploy(_) | TxPayload::ContractCall(_)
672        )
673    }
674
675    /// Get token data if this is a Token transaction
676    pub fn token_data(&self) -> Option<&TokenTxData> {
677        match &self.payload {
678            TxPayload::Token(data) => Some(data),
679            _ => None,
680        }
681    }
682
683    /// Get staking data if this is a Staking transaction
684    pub fn staking_data(&self) -> Option<&StakingTxData> {
685        match &self.payload {
686            TxPayload::Staking(data) => Some(data),
687            _ => None,
688        }
689    }
690
691    /// Check if this is a staking transaction
692    pub fn is_staking(&self) -> bool {
693        matches!(self.payload, TxPayload::Staking(_))
694    }
695
696    /// Get messaging data if this is a Messaging transaction
697    pub fn messaging_data(&self) -> Option<&MessagingTxData> {
698        match &self.payload {
699            TxPayload::Messaging(data) => Some(data),
700            _ => None,
701        }
702    }
703
704    /// Check if this is a messaging transaction
705    pub fn is_messaging(&self) -> bool {
706        matches!(self.payload, TxPayload::Messaging(_))
707    }
708
709    /// Get docclass data if this is a DocClass transaction
710    pub fn docclass_data(&self) -> Option<&DocClassTxData> {
711        match &self.payload {
712            TxPayload::DocClass(data) => Some(data),
713            _ => None,
714        }
715    }
716
717    /// Check if this is a docclass transaction
718    pub fn is_docclass(&self) -> bool {
719        matches!(self.payload, TxPayload::DocClass(_))
720    }
721}
722
723impl Transaction {
724    /// Create a new transaction
725    pub fn new(
726        chain_id: ChainId,
727        from: Address,
728        to: Address,
729        amount: Balance,
730        fee: Balance,
731        nonce: Nonce,
732    ) -> Self {
733        Self {
734            chain_id,
735            from,
736            to,
737            amount,
738            fee,
739            nonce,
740        }
741    }
742
743    /// Compute the signing hash for this transaction
744    /// This is what gets signed by the sender
745    pub fn signing_hash(&self) -> Hash {
746        // Deterministic serialization using bincode
747        let bytes = bincode::serialize(self).expect("Transaction serialization should not fail");
748        Hash::hash(&bytes)
749    }
750
751    /// Serialize transaction to bytes
752    pub fn to_bytes(&self) -> Vec<u8> {
753        bincode::serialize(self).expect("Transaction serialization should not fail")
754    }
755
756    /// Deserialize transaction from bytes
757    pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
758        bincode::deserialize(bytes)
759    }
760
761    /// Total cost to sender (amount + fee)
762    pub fn total_cost(&self) -> Balance {
763        self.amount.saturating_add(self.fee)
764    }
765}
766
767/// Transaction inner payload - supports both legacy and V2 formats
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
769pub enum TxInner {
770    /// Legacy transfer transaction (backwards compatible)
771    Legacy(Transaction),
772    /// V2 transaction with extended payload support (NFT, etc.)
773    V2(TransactionV2),
774}
775
776/// Signed transaction (transaction + signature)
777/// Supports both legacy transfers and V2 transactions (NFT operations)
778#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
779pub struct SignedTransaction {
780    /// The unsigned transaction (legacy or V2)
781    pub inner: TxInner,
782    /// Ed25519 signature (64 bytes)
783    #[serde(with = "BigArray")]
784    pub signature: [u8; 64],
785    /// Signer's public key (for verification)
786    pub public_key: [u8; 32],
787}
788
789impl SignedTransaction {
790    /// Create a new signed legacy transaction (backwards compatible)
791    pub fn new(tx: Transaction, signature: [u8; 64], public_key: [u8; 32]) -> Self {
792        Self {
793            inner: TxInner::Legacy(tx),
794            signature,
795            public_key,
796        }
797    }
798
799    /// Create a new signed V2 transaction (for NFT and extended operations)
800    pub fn new_v2(tx: TransactionV2, signature: [u8; 64], public_key: [u8; 32]) -> Self {
801        Self {
802            inner: TxInner::V2(tx),
803            signature,
804            public_key,
805        }
806    }
807
808    /// Get the transaction type
809    pub fn tx_type(&self) -> TxType {
810        match &self.inner {
811            TxInner::Legacy(_) => TxType::Transfer,
812            TxInner::V2(tx) => tx.tx_type(),
813        }
814    }
815
816    /// Check if this is an NFT transaction
817    pub fn is_nft(&self) -> bool {
818        self.tx_type() == TxType::Nft
819    }
820
821    /// Check if this is a Token (SRC-20) transaction
822    pub fn is_token(&self) -> bool {
823        self.tx_type() == TxType::Token
824    }
825
826    /// Get NFT data if this is an NFT transaction
827    pub fn nft_data(&self) -> Option<&NftTxData> {
828        match &self.inner {
829            TxInner::V2(tx) => match &tx.payload {
830                TxPayload::Nft(data) => Some(data),
831                _ => None,
832            },
833            _ => None,
834        }
835    }
836
837    /// Get Token data if this is a Token transaction
838    pub fn token_data(&self) -> Option<&TokenTxData> {
839        match &self.inner {
840            TxInner::V2(tx) => match &tx.payload {
841                TxPayload::Token(data) => Some(data),
842                _ => None,
843            },
844            _ => None,
845        }
846    }
847
848    /// Get Staking data if this is a Staking transaction
849    pub fn staking_data(&self) -> Option<&StakingTxData> {
850        match &self.inner {
851            TxInner::V2(tx) => match &tx.payload {
852                TxPayload::Staking(data) => Some(data),
853                _ => None,
854            },
855            _ => None,
856        }
857    }
858
859    /// Check if this is a Staking transaction
860    pub fn is_staking(&self) -> bool {
861        self.tx_type() == TxType::Staking
862    }
863
864    /// Get Messaging data if this is a Messaging transaction
865    pub fn messaging_data(&self) -> Option<&MessagingTxData> {
866        match &self.inner {
867            TxInner::V2(tx) => match &tx.payload {
868                TxPayload::Messaging(data) => Some(data),
869                _ => None,
870            },
871            _ => None,
872        }
873    }
874
875    /// Check if this is a Messaging transaction
876    pub fn is_messaging(&self) -> bool {
877        self.tx_type() == TxType::Messaging
878    }
879
880    /// Compute the transaction hash (unique identifier)
881    pub fn hash(&self) -> Hash {
882        let bytes =
883            bincode::serialize(self).expect("SignedTransaction serialization should not fail");
884        Hash::hash(&bytes)
885    }
886
887    /// Get the transaction signing hash
888    pub fn signing_hash(&self) -> Hash {
889        match &self.inner {
890            TxInner::Legacy(tx) => tx.signing_hash(),
891            TxInner::V2(tx) => tx.signing_hash(),
892        }
893    }
894
895    /// Serialize to bytes
896    pub fn to_bytes(&self) -> Vec<u8> {
897        bincode::serialize(self).expect("SignedTransaction serialization should not fail")
898    }
899
900    /// Deserialize from bytes
901    pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
902        bincode::deserialize(bytes)
903    }
904
905    /// Serialize to hex string
906    pub fn to_hex(&self) -> String {
907        hex::encode(self.to_bytes())
908    }
909
910    /// Deserialize from hex string
911    pub fn from_hex(s: &str) -> Result<Self, String> {
912        let s = s.strip_prefix("0x").unwrap_or(s);
913        let bytes = hex::decode(s).map_err(|e| e.to_string())?;
914        Self::from_bytes(&bytes).map_err(|e| e.to_string())
915    }
916
917    /// Get sender address
918    pub fn sender(&self) -> Address {
919        match &self.inner {
920            TxInner::Legacy(tx) => tx.from,
921            TxInner::V2(tx) => tx.from,
922        }
923    }
924
925    /// Get chain ID
926    pub fn chain_id(&self) -> ChainId {
927        match &self.inner {
928            TxInner::Legacy(tx) => tx.chain_id,
929            TxInner::V2(tx) => tx.chain_id,
930        }
931    }
932
933    /// Get transaction fee
934    pub fn fee(&self) -> Balance {
935        match &self.inner {
936            TxInner::Legacy(tx) => tx.fee,
937            TxInner::V2(tx) => tx.fee,
938        }
939    }
940
941    /// Get transaction nonce
942    pub fn nonce(&self) -> Nonce {
943        match &self.inner {
944            TxInner::Legacy(tx) => tx.nonce,
945            TxInner::V2(tx) => tx.nonce,
946        }
947    }
948
949    /// Get transfer amount (0 for NFT transactions)
950    pub fn amount(&self) -> Balance {
951        match &self.inner {
952            TxInner::Legacy(tx) => tx.amount,
953            TxInner::V2(tx) => tx.amount(),
954        }
955    }
956
957    /// Get recipient address (None for NFT transactions)
958    pub fn recipient(&self) -> Option<Address> {
959        match &self.inner {
960            TxInner::Legacy(tx) => Some(tx.to),
961            TxInner::V2(tx) => tx.recipient(),
962        }
963    }
964
965    /// Get the expected address from the public key
966    pub fn signer_address(&self) -> Address {
967        Address::from_public_key(&self.public_key)
968    }
969
970    /// Verify that the signer matches the from address
971    pub fn verify_signer(&self) -> bool {
972        self.signer_address() == self.sender()
973    }
974
975    /// Get legacy transaction reference (for backwards compatibility)
976    /// Returns None if this is a V2 NFT transaction
977    pub fn legacy_tx(&self) -> Option<&Transaction> {
978        match &self.inner {
979            TxInner::Legacy(tx) => Some(tx),
980            TxInner::V2(_) => None,
981        }
982    }
983
984    /// Access the inner transaction data
985    /// Use tx_type() to determine which variant is active
986    pub fn inner(&self) -> &TxInner {
987        &self.inner
988    }
989}
990
991// Backwards compatibility: provide access to legacy `tx` field
992impl SignedTransaction {
993    /// Get legacy transaction (DEPRECATED: use sender(), fee(), nonce() etc. or inner() instead)
994    /// Panics if this is a V2 NFT transaction
995    #[deprecated(note = "Use sender(), fee(), nonce() etc. or inner() instead")]
996    pub fn tx(&self) -> &Transaction {
997        match &self.inner {
998            TxInner::Legacy(tx) => tx,
999            TxInner::V2(_) => panic!("Cannot access legacy tx field on V2 transaction"),
1000        }
1001    }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006    use super::*;
1007
1008    fn sample_tx() -> Transaction {
1009        Transaction::new(
1010            1, // chain_id
1011            Address::from_hex("0x0000000000000000000000000000000000000001").unwrap(),
1012            Address::from_hex("0x0000000000000000000000000000000000000002").unwrap(),
1013            1000,
1014            10,
1015            0,
1016        )
1017    }
1018
1019    #[test]
1020    fn test_signing_hash_deterministic() {
1021        let tx = sample_tx();
1022        let h1 = tx.signing_hash();
1023        let h2 = tx.signing_hash();
1024        assert_eq!(h1, h2);
1025    }
1026
1027    #[test]
1028    fn test_different_nonce_different_hash() {
1029        let tx1 = sample_tx();
1030        let mut tx2 = sample_tx();
1031        tx2.nonce = 1;
1032        assert_ne!(tx1.signing_hash(), tx2.signing_hash());
1033    }
1034
1035    #[test]
1036    fn test_total_cost() {
1037        let tx = sample_tx();
1038        assert_eq!(tx.total_cost(), 1010);
1039    }
1040
1041    #[test]
1042    fn test_serialization_roundtrip() {
1043        let tx = sample_tx();
1044        let bytes = tx.to_bytes();
1045        let tx2 = Transaction::from_bytes(&bytes).unwrap();
1046        assert_eq!(tx, tx2);
1047    }
1048
1049    #[test]
1050    fn test_signed_tx_hex_roundtrip() {
1051        let tx = sample_tx();
1052        let signed = SignedTransaction::new(tx, [0u8; 64], [0u8; 32]);
1053        let hex = signed.to_hex();
1054        let signed2 = SignedTransaction::from_hex(&hex).unwrap();
1055        assert_eq!(signed, signed2);
1056    }
1057}