guts_consensus/
transaction.rs

1//! Consensus transaction types.
2//!
3//! All state-changing operations in Guts are represented as transactions
4//! that go through the consensus layer for total ordering.
5
6use guts_storage::ObjectId;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10/// Serializable public key (hex-encoded Ed25519 public key).
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct SerializablePublicKey(pub String);
13
14impl SerializablePublicKey {
15    /// Creates from a hex string.
16    pub fn from_hex(hex: impl Into<String>) -> Self {
17        Self(hex.into())
18    }
19
20    /// Creates from a commonware public key.
21    pub fn from_pubkey(pk: &commonware_cryptography::ed25519::PublicKey) -> Self {
22        Self(hex::encode(pk.as_ref()))
23    }
24
25    /// Converts to a commonware public key.
26    pub fn to_pubkey(
27        &self,
28    ) -> Result<commonware_cryptography::ed25519::PublicKey, hex::FromHexError> {
29        let bytes = hex::decode(&self.0)?;
30        if bytes.len() != 32 {
31            return Err(hex::FromHexError::InvalidStringLength);
32        }
33        let mut arr = [0u8; 32];
34        arr.copy_from_slice(&bytes);
35        // Create via ed25519_consensus VerificationKey which implements Into<PublicKey>
36        let vk = ed25519_consensus::VerificationKey::try_from(arr)
37            .map_err(|_| hex::FromHexError::InvalidStringLength)?;
38        Ok(commonware_cryptography::ed25519::PublicKey::from(vk))
39    }
40
41    /// Returns the hex string as a reference.
42    pub fn as_hex(&self) -> &str {
43        &self.0
44    }
45
46    /// Returns the hex string as an owned String.
47    pub fn to_hex(&self) -> String {
48        self.0.clone()
49    }
50}
51
52impl std::fmt::Display for SerializablePublicKey {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "{}", self.0)
55    }
56}
57
58/// Serializable signature (hex-encoded Ed25519 signature).
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct SerializableSignature(pub String);
61
62impl SerializableSignature {
63    /// Creates from a hex string.
64    pub fn from_hex(hex: impl Into<String>) -> Self {
65        Self(hex.into())
66    }
67
68    /// Creates from a commonware signature.
69    pub fn from_signature(sig: &commonware_cryptography::ed25519::Signature) -> Self {
70        Self(hex::encode(sig.as_ref()))
71    }
72
73    /// Converts to a commonware signature.
74    pub fn to_signature(
75        &self,
76    ) -> Result<commonware_cryptography::ed25519::Signature, hex::FromHexError> {
77        let bytes = hex::decode(&self.0)?;
78        if bytes.len() != 64 {
79            return Err(hex::FromHexError::InvalidStringLength);
80        }
81        let mut arr = [0u8; 64];
82        arr.copy_from_slice(&bytes);
83        // Create via ed25519_consensus Signature which implements Into<Signature>
84        let sig = ed25519_consensus::Signature::from(arr);
85        Ok(commonware_cryptography::ed25519::Signature::from(sig))
86    }
87
88    /// Returns the hex string.
89    pub fn as_hex(&self) -> &str {
90        &self.0
91    }
92}
93
94impl std::fmt::Display for SerializableSignature {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        write!(f, "{}", self.0)
97    }
98}
99
100/// Update to a pull request.
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102pub struct PullRequestUpdate {
103    /// New title (if changed).
104    pub title: Option<String>,
105    /// New description (if changed).
106    pub description: Option<String>,
107    /// New state (open, closed, merged).
108    pub state: Option<String>,
109    /// Labels to add.
110    pub labels_add: Vec<String>,
111    /// Labels to remove.
112    pub labels_remove: Vec<String>,
113    /// Merged by (for merge operations).
114    pub merged_by: Option<String>,
115}
116
117/// Update to an issue.
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct IssueUpdate {
120    /// New title (if changed).
121    pub title: Option<String>,
122    /// New description (if changed).
123    pub description: Option<String>,
124    /// New state (open, closed).
125    pub state: Option<String>,
126    /// Labels to add.
127    pub labels_add: Vec<String>,
128    /// Labels to remove.
129    pub labels_remove: Vec<String>,
130    /// Closed by (for close operations).
131    pub closed_by: Option<String>,
132}
133
134/// Update to an organization.
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
136pub struct OrgUpdate {
137    /// New display name.
138    pub display_name: Option<String>,
139    /// New description.
140    pub description: Option<String>,
141}
142
143/// Comment target specification.
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
145pub enum CommentTargetSpec {
146    /// Comment on a pull request.
147    PullRequest { repo_key: String, number: u32 },
148    /// Comment on an issue.
149    Issue { repo_key: String, number: u32 },
150}
151
152/// Serializable branch protection rules for consensus.
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct BranchProtectionSpec {
155    /// Require pull request for changes.
156    pub require_pr: bool,
157    /// Number of required reviews.
158    pub required_reviews: u32,
159    /// Require signed commits.
160    pub require_signed_commits: bool,
161    /// Allow force pushes.
162    pub allow_force_push: bool,
163    /// Allow deletions.
164    pub allow_deletions: bool,
165    /// Required status checks.
166    pub required_status_checks: Vec<String>,
167}
168
169/// Serializable team specification for consensus.
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171pub struct TeamSpec {
172    /// Team name (slug).
173    pub name: String,
174    /// Team description.
175    pub description: Option<String>,
176    /// Team permission level.
177    pub permission: String,
178}
179
180/// A unique transaction identifier (SHA-256 hash of the transaction).
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
182pub struct TransactionId([u8; 32]);
183
184impl TransactionId {
185    /// Creates a transaction ID from raw bytes.
186    pub fn from_bytes(bytes: [u8; 32]) -> Self {
187        Self(bytes)
188    }
189
190    /// Returns the raw bytes.
191    pub fn as_bytes(&self) -> &[u8; 32] {
192        &self.0
193    }
194
195    /// Returns the hex representation.
196    pub fn to_hex(&self) -> String {
197        hex::encode(self.0)
198    }
199
200    /// Creates a transaction ID from a hex string.
201    pub fn from_hex(hex_str: &str) -> Result<Self, hex::FromHexError> {
202        let mut bytes = [0u8; 32];
203        hex::decode_to_slice(hex_str, &mut bytes)?;
204        Ok(Self(bytes))
205    }
206}
207
208impl std::fmt::Display for TransactionId {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        write!(f, "{}", self.to_hex())
211    }
212}
213
214/// Transactions that require consensus ordering.
215///
216/// Every state-changing operation in Guts is represented as a transaction.
217/// Transactions are ordered by the consensus layer and applied atomically.
218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
219pub enum Transaction {
220    // ==================== Git Operations ====================
221    /// Push operation updating references.
222    GitPush {
223        /// Repository key (owner/name).
224        repo_key: String,
225        /// Reference being updated (e.g., "refs/heads/main").
226        ref_name: String,
227        /// Old object ID (for optimistic locking).
228        old_oid: ObjectId,
229        /// New object ID.
230        new_oid: ObjectId,
231        /// List of new objects being added.
232        objects: Vec<ObjectId>,
233        /// Signer's public key.
234        signer: SerializablePublicKey,
235        /// Signature over transaction data.
236        signature: SerializableSignature,
237    },
238
239    // ==================== Repository Management ====================
240    /// Create a new repository.
241    CreateRepository {
242        /// Owner (user or organization).
243        owner: String,
244        /// Repository name.
245        name: String,
246        /// Description.
247        description: String,
248        /// Default branch name.
249        default_branch: String,
250        /// Visibility (public/private).
251        visibility: String,
252        /// Creator's public key.
253        creator: SerializablePublicKey,
254        /// Signature over transaction data.
255        signature: SerializableSignature,
256    },
257
258    /// Delete a repository.
259    DeleteRepository {
260        /// Repository key (owner/name).
261        repo_key: String,
262        /// Deleter's public key.
263        deleter: SerializablePublicKey,
264        /// Signature over transaction data.
265        signature: SerializableSignature,
266    },
267
268    // ==================== Collaboration - Pull Requests ====================
269    /// Create a new pull request.
270    CreatePullRequest {
271        /// Repository key.
272        repo_key: String,
273        /// PR title.
274        title: String,
275        /// PR description.
276        description: String,
277        /// Author username.
278        author: String,
279        /// Source branch.
280        source_branch: String,
281        /// Target branch.
282        target_branch: String,
283        /// Source commit hash.
284        source_commit: ObjectId,
285        /// Target commit hash.
286        target_commit: ObjectId,
287        /// Signer's public key.
288        signer: SerializablePublicKey,
289        /// Signature.
290        signature: SerializableSignature,
291    },
292
293    /// Update an existing pull request.
294    UpdatePullRequest {
295        /// Repository key.
296        repo_key: String,
297        /// PR number.
298        pr_number: u32,
299        /// Update details.
300        update: PullRequestUpdate,
301        /// Signer's public key.
302        signer: SerializablePublicKey,
303        /// Signature.
304        signature: SerializableSignature,
305    },
306
307    /// Merge a pull request.
308    MergePullRequest {
309        /// Repository key.
310        repo_key: String,
311        /// PR number.
312        pr_number: u32,
313        /// Merge commit object ID.
314        merge_commit: ObjectId,
315        /// Person performing the merge.
316        merged_by: String,
317        /// Signer's public key.
318        signer: SerializablePublicKey,
319        /// Signature.
320        signature: SerializableSignature,
321    },
322
323    // ==================== Collaboration - Issues ====================
324    /// Create a new issue.
325    CreateIssue {
326        /// Repository key.
327        repo_key: String,
328        /// Issue title.
329        title: String,
330        /// Issue description.
331        description: String,
332        /// Author username.
333        author: String,
334        /// Signer's public key.
335        signer: SerializablePublicKey,
336        /// Signature.
337        signature: SerializableSignature,
338    },
339
340    /// Update an existing issue.
341    UpdateIssue {
342        /// Repository key.
343        repo_key: String,
344        /// Issue number.
345        issue_number: u32,
346        /// Update details.
347        update: IssueUpdate,
348        /// Signer's public key.
349        signer: SerializablePublicKey,
350        /// Signature.
351        signature: SerializableSignature,
352    },
353
354    // ==================== Collaboration - Comments & Reviews ====================
355    /// Create a comment on a PR or issue.
356    CreateComment {
357        /// Comment target (PR or issue).
358        target: CommentTargetSpec,
359        /// Author username.
360        author: String,
361        /// Comment body.
362        body: String,
363        /// Signer's public key.
364        signer: SerializablePublicKey,
365        /// Signature.
366        signature: SerializableSignature,
367    },
368
369    /// Create a review on a pull request.
370    CreateReview {
371        /// Repository key.
372        repo_key: String,
373        /// PR number.
374        pr_number: u32,
375        /// Reviewer username.
376        author: String,
377        /// Review state (approved, changes_requested, commented).
378        state: String,
379        /// Optional review body.
380        body: Option<String>,
381        /// Commit ID being reviewed.
382        commit_id: String,
383        /// Signer's public key.
384        signer: SerializablePublicKey,
385        /// Signature.
386        signature: SerializableSignature,
387    },
388
389    // ==================== Governance - Organizations ====================
390    /// Create a new organization.
391    CreateOrganization {
392        /// Organization slug (unique name).
393        name: String,
394        /// Display name.
395        display_name: String,
396        /// Creator's public key.
397        creator: SerializablePublicKey,
398        /// Signature.
399        signature: SerializableSignature,
400    },
401
402    /// Update an organization.
403    UpdateOrganization {
404        /// Organization name.
405        org_name: String,
406        /// Update details.
407        update: OrgUpdate,
408        /// Signer's public key.
409        signer: SerializablePublicKey,
410        /// Signature.
411        signature: SerializableSignature,
412    },
413
414    /// Add a member to an organization.
415    AddOrgMember {
416        /// Organization name.
417        org_name: String,
418        /// Member's public key.
419        member: SerializablePublicKey,
420        /// Role (owner, admin, member).
421        role: String,
422        /// Signer's public key (admin performing action).
423        signer: SerializablePublicKey,
424        /// Signature.
425        signature: SerializableSignature,
426    },
427
428    /// Remove a member from an organization.
429    RemoveOrgMember {
430        /// Organization name.
431        org_name: String,
432        /// Member's public key.
433        member: SerializablePublicKey,
434        /// Signer's public key (admin performing action).
435        signer: SerializablePublicKey,
436        /// Signature.
437        signature: SerializableSignature,
438    },
439
440    // ==================== Governance - Teams ====================
441    /// Create a new team.
442    CreateTeam {
443        /// Organization name.
444        org_name: String,
445        /// Team specification.
446        team: TeamSpec,
447        /// Signer's public key.
448        signer: SerializablePublicKey,
449        /// Signature.
450        signature: SerializableSignature,
451    },
452
453    /// Delete a team.
454    DeleteTeam {
455        /// Organization name.
456        org_name: String,
457        /// Team name.
458        team_name: String,
459        /// Signer's public key.
460        signer: SerializablePublicKey,
461        /// Signature.
462        signature: SerializableSignature,
463    },
464
465    /// Add a member to a team.
466    AddTeamMember {
467        /// Organization name.
468        org_name: String,
469        /// Team name.
470        team_name: String,
471        /// Member's public key.
472        member: SerializablePublicKey,
473        /// Signer's public key.
474        signer: SerializablePublicKey,
475        /// Signature.
476        signature: SerializableSignature,
477    },
478
479    /// Remove a member from a team.
480    RemoveTeamMember {
481        /// Organization name.
482        org_name: String,
483        /// Team name.
484        team_name: String,
485        /// Member's public key.
486        member: SerializablePublicKey,
487        /// Signer's public key.
488        signer: SerializablePublicKey,
489        /// Signature.
490        signature: SerializableSignature,
491    },
492
493    /// Add a repository to a team.
494    AddTeamRepo {
495        /// Organization name.
496        org_name: String,
497        /// Team name.
498        team_name: String,
499        /// Repository key.
500        repo_key: String,
501        /// Signer's public key.
502        signer: SerializablePublicKey,
503        /// Signature.
504        signature: SerializableSignature,
505    },
506
507    // ==================== Governance - Permissions ====================
508    /// Add or update a collaborator on a repository.
509    SetCollaborator {
510        /// Repository key.
511        repo_key: String,
512        /// Collaborator's username or public key.
513        collaborator: String,
514        /// Permission level (read, write, admin).
515        permission: String,
516        /// Signer's public key.
517        signer: SerializablePublicKey,
518        /// Signature.
519        signature: SerializableSignature,
520    },
521
522    /// Remove a collaborator from a repository.
523    RemoveCollaborator {
524        /// Repository key.
525        repo_key: String,
526        /// Collaborator's username or public key.
527        collaborator: String,
528        /// Signer's public key.
529        signer: SerializablePublicKey,
530        /// Signature.
531        signature: SerializableSignature,
532    },
533
534    // ==================== Governance - Branch Protection ====================
535    /// Set branch protection rules.
536    SetBranchProtection {
537        /// Repository key.
538        repo_key: String,
539        /// Branch name (pattern).
540        branch: String,
541        /// Protection rules.
542        protection: BranchProtectionSpec,
543        /// Signer's public key.
544        signer: SerializablePublicKey,
545        /// Signature.
546        signature: SerializableSignature,
547    },
548
549    /// Remove branch protection rules.
550    RemoveBranchProtection {
551        /// Repository key.
552        repo_key: String,
553        /// Branch name (pattern).
554        branch: String,
555        /// Signer's public key.
556        signer: SerializablePublicKey,
557        /// Signature.
558        signature: SerializableSignature,
559    },
560}
561
562impl Transaction {
563    /// Computes the unique transaction ID (SHA-256 hash of serialized transaction).
564    pub fn id(&self) -> TransactionId {
565        let bytes = serde_json::to_vec(self).expect("transaction serialization should not fail");
566        let mut hasher = Sha256::new();
567        hasher.update(&bytes);
568        let result = hasher.finalize();
569        let mut id = [0u8; 32];
570        id.copy_from_slice(&result);
571        TransactionId(id)
572    }
573
574    /// Returns the signer's public key for this transaction.
575    pub fn signer(&self) -> &SerializablePublicKey {
576        match self {
577            Transaction::GitPush { signer, .. } => signer,
578            Transaction::CreateRepository { creator, .. } => creator,
579            Transaction::DeleteRepository { deleter, .. } => deleter,
580            Transaction::CreatePullRequest { signer, .. } => signer,
581            Transaction::UpdatePullRequest { signer, .. } => signer,
582            Transaction::MergePullRequest { signer, .. } => signer,
583            Transaction::CreateIssue { signer, .. } => signer,
584            Transaction::UpdateIssue { signer, .. } => signer,
585            Transaction::CreateComment { signer, .. } => signer,
586            Transaction::CreateReview { signer, .. } => signer,
587            Transaction::CreateOrganization { creator, .. } => creator,
588            Transaction::UpdateOrganization { signer, .. } => signer,
589            Transaction::AddOrgMember { signer, .. } => signer,
590            Transaction::RemoveOrgMember { signer, .. } => signer,
591            Transaction::CreateTeam { signer, .. } => signer,
592            Transaction::DeleteTeam { signer, .. } => signer,
593            Transaction::AddTeamMember { signer, .. } => signer,
594            Transaction::RemoveTeamMember { signer, .. } => signer,
595            Transaction::AddTeamRepo { signer, .. } => signer,
596            Transaction::SetCollaborator { signer, .. } => signer,
597            Transaction::RemoveCollaborator { signer, .. } => signer,
598            Transaction::SetBranchProtection { signer, .. } => signer,
599            Transaction::RemoveBranchProtection { signer, .. } => signer,
600        }
601    }
602
603    /// Returns the signature for this transaction.
604    pub fn signature(&self) -> &SerializableSignature {
605        match self {
606            Transaction::GitPush { signature, .. } => signature,
607            Transaction::CreateRepository { signature, .. } => signature,
608            Transaction::DeleteRepository { signature, .. } => signature,
609            Transaction::CreatePullRequest { signature, .. } => signature,
610            Transaction::UpdatePullRequest { signature, .. } => signature,
611            Transaction::MergePullRequest { signature, .. } => signature,
612            Transaction::CreateIssue { signature, .. } => signature,
613            Transaction::UpdateIssue { signature, .. } => signature,
614            Transaction::CreateComment { signature, .. } => signature,
615            Transaction::CreateReview { signature, .. } => signature,
616            Transaction::CreateOrganization { signature, .. } => signature,
617            Transaction::UpdateOrganization { signature, .. } => signature,
618            Transaction::AddOrgMember { signature, .. } => signature,
619            Transaction::RemoveOrgMember { signature, .. } => signature,
620            Transaction::CreateTeam { signature, .. } => signature,
621            Transaction::DeleteTeam { signature, .. } => signature,
622            Transaction::AddTeamMember { signature, .. } => signature,
623            Transaction::RemoveTeamMember { signature, .. } => signature,
624            Transaction::AddTeamRepo { signature, .. } => signature,
625            Transaction::SetCollaborator { signature, .. } => signature,
626            Transaction::RemoveCollaborator { signature, .. } => signature,
627            Transaction::SetBranchProtection { signature, .. } => signature,
628            Transaction::RemoveBranchProtection { signature, .. } => signature,
629        }
630    }
631
632    /// Returns the affected repository key, if any.
633    pub fn repo_key(&self) -> Option<&str> {
634        match self {
635            Transaction::GitPush { repo_key, .. } => Some(repo_key),
636            Transaction::CreateRepository { .. } => None, // Key is owner/name, returned separately
637            Transaction::DeleteRepository { repo_key, .. } => Some(repo_key),
638            Transaction::CreatePullRequest { repo_key, .. } => Some(repo_key),
639            Transaction::UpdatePullRequest { repo_key, .. } => Some(repo_key),
640            Transaction::MergePullRequest { repo_key, .. } => Some(repo_key),
641            Transaction::CreateIssue { repo_key, .. } => Some(repo_key),
642            Transaction::UpdateIssue { repo_key, .. } => Some(repo_key),
643            Transaction::CreateComment { target, .. } => match target {
644                CommentTargetSpec::PullRequest { repo_key, .. } => Some(repo_key),
645                CommentTargetSpec::Issue { repo_key, .. } => Some(repo_key),
646            },
647            Transaction::CreateReview { repo_key, .. } => Some(repo_key),
648            Transaction::SetCollaborator { repo_key, .. } => Some(repo_key),
649            Transaction::RemoveCollaborator { repo_key, .. } => Some(repo_key),
650            Transaction::SetBranchProtection { repo_key, .. } => Some(repo_key),
651            Transaction::RemoveBranchProtection { repo_key, .. } => Some(repo_key),
652            Transaction::AddTeamRepo { repo_key, .. } => Some(repo_key),
653            _ => None, // Org/Team transactions don't have a repo_key
654        }
655    }
656
657    /// Returns a human-readable description of the transaction type.
658    pub fn kind(&self) -> &'static str {
659        match self {
660            Transaction::GitPush { .. } => "git_push",
661            Transaction::CreateRepository { .. } => "create_repository",
662            Transaction::DeleteRepository { .. } => "delete_repository",
663            Transaction::CreatePullRequest { .. } => "create_pull_request",
664            Transaction::UpdatePullRequest { .. } => "update_pull_request",
665            Transaction::MergePullRequest { .. } => "merge_pull_request",
666            Transaction::CreateIssue { .. } => "create_issue",
667            Transaction::UpdateIssue { .. } => "update_issue",
668            Transaction::CreateComment { .. } => "create_comment",
669            Transaction::CreateReview { .. } => "create_review",
670            Transaction::CreateOrganization { .. } => "create_organization",
671            Transaction::UpdateOrganization { .. } => "update_organization",
672            Transaction::AddOrgMember { .. } => "add_org_member",
673            Transaction::RemoveOrgMember { .. } => "remove_org_member",
674            Transaction::CreateTeam { .. } => "create_team",
675            Transaction::DeleteTeam { .. } => "delete_team",
676            Transaction::AddTeamMember { .. } => "add_team_member",
677            Transaction::RemoveTeamMember { .. } => "remove_team_member",
678            Transaction::AddTeamRepo { .. } => "add_team_repo",
679            Transaction::SetCollaborator { .. } => "set_collaborator",
680            Transaction::RemoveCollaborator { .. } => "remove_collaborator",
681            Transaction::SetBranchProtection { .. } => "set_branch_protection",
682            Transaction::RemoveBranchProtection { .. } => "remove_branch_protection",
683        }
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use commonware_cryptography::{ed25519, PrivateKeyExt, Signer};
691
692    fn test_keypair() -> (SerializablePublicKey, SerializableSignature) {
693        let key = ed25519::PrivateKey::from_seed(42);
694        let sig = key.sign(Some(b"_GUTS"), b"test");
695        (
696            SerializablePublicKey::from_pubkey(&key.public_key()),
697            SerializableSignature::from_signature(&sig),
698        )
699    }
700
701    #[test]
702    fn test_transaction_id_roundtrip() {
703        let bytes = [0xab; 32];
704        let id = TransactionId::from_bytes(bytes);
705        assert_eq!(id.as_bytes(), &bytes);
706
707        let hex = id.to_hex();
708        let parsed = TransactionId::from_hex(&hex).unwrap();
709        assert_eq!(id, parsed);
710    }
711
712    #[test]
713    fn test_transaction_id_display() {
714        let id = TransactionId::from_bytes([0; 32]);
715        assert_eq!(format!("{}", id), "0".repeat(64));
716    }
717
718    #[test]
719    fn test_transaction_kind() {
720        let (signer, signature) = test_keypair();
721
722        let tx = Transaction::CreateRepository {
723            owner: "alice".into(),
724            name: "test".into(),
725            description: "A test repo".into(),
726            default_branch: "main".into(),
727            visibility: "public".into(),
728            creator: signer,
729            signature,
730        };
731
732        assert_eq!(tx.kind(), "create_repository");
733    }
734
735    #[test]
736    fn test_transaction_id_unique() {
737        let (signer, signature) = test_keypair();
738
739        let tx1 = Transaction::CreateRepository {
740            owner: "alice".into(),
741            name: "test1".into(),
742            description: "A test repo".into(),
743            default_branch: "main".into(),
744            visibility: "public".into(),
745            creator: signer.clone(),
746            signature: signature.clone(),
747        };
748
749        let tx2 = Transaction::CreateRepository {
750            owner: "alice".into(),
751            name: "test2".into(), // Different name
752            description: "A test repo".into(),
753            default_branch: "main".into(),
754            visibility: "public".into(),
755            creator: signer,
756            signature,
757        };
758
759        assert_ne!(tx1.id(), tx2.id());
760    }
761
762    #[test]
763    fn test_serializable_pubkey_roundtrip() {
764        let key = ed25519::PrivateKey::from_seed(42);
765        let pk = key.public_key();
766        let serializable = SerializablePublicKey::from_pubkey(&pk);
767        let recovered = serializable.to_pubkey().unwrap();
768        assert_eq!(pk, recovered);
769    }
770
771    #[test]
772    fn test_serializable_signature_roundtrip() {
773        let key = ed25519::PrivateKey::from_seed(42);
774        let sig = key.sign(Some(b"_GUTS"), b"test");
775        let serializable = SerializableSignature::from_signature(&sig);
776        let recovered = serializable.to_signature().unwrap();
777        assert_eq!(sig, recovered);
778    }
779
780    #[test]
781    fn test_transaction_serialization() {
782        let (signer, signature) = test_keypair();
783
784        let tx = Transaction::CreateRepository {
785            owner: "alice".into(),
786            name: "test".into(),
787            description: "A test repo".into(),
788            default_branch: "main".into(),
789            visibility: "public".into(),
790            creator: signer,
791            signature,
792        };
793
794        let json = serde_json::to_string(&tx).unwrap();
795        let parsed: Transaction = serde_json::from_str(&json).unwrap();
796        assert_eq!(tx.id(), parsed.id());
797    }
798}