did_method_plc/
op_builder.rs

1use std::collections::HashMap;
2
3use crate::{
4    operation::{PLCOperation, PLCOperationType, SignedPLCOperation, UnsignedOperation, UnsignedPLCOperation},
5    util::{assure_at_prefix, assure_http},
6    Keypair, PLCError, Service, DIDPLC,
7};
8
9pub struct OperationBuilder<'a, 'k> {
10    plc: &'a DIDPLC,
11    key: Option<&'k Keypair>,
12    did: Option<String>,
13    rotation_keys: Vec<String>,
14    services: HashMap<String, Service>,
15    also_known_as: Vec<String>,
16    verification_methods: HashMap<String, String>,
17    prev: Option<String>,
18}
19
20impl<'a, 'k> OperationBuilder<'a, 'k> {
21    pub fn new(plc: &'a DIDPLC) -> Self {
22        OperationBuilder {
23            plc,
24            key: None,
25            did: None,
26            rotation_keys: vec![],
27            services: HashMap::new(),
28            also_known_as: vec![],
29            verification_methods: HashMap::new(),
30            prev: None,
31        }
32    }
33
34    pub fn for_did(plc: &'a DIDPLC, did: String) -> Self {
35        OperationBuilder {
36            plc,
37            key: None,
38            did: Some(did),
39            rotation_keys: vec![],
40            services: HashMap::new(),
41            also_known_as: vec![],
42            verification_methods: HashMap::new(),
43            prev: None,
44        }
45    }
46
47    pub async fn from_did_state(plc: &'a DIDPLC, did: String) -> Result<Self, PLCError> {
48        let state = plc.get_current_state(&did).await?;
49        match state {
50            PLCOperation::UnsignedPLC(op) => {
51                Ok(OperationBuilder {
52                    plc,
53                    key: None,
54                    did: Some(did),
55                    rotation_keys: op.rotation_keys.clone(),
56                    services: op.services.clone(),
57                    also_known_as: op.also_known_as.clone(),
58                    verification_methods: op.verification_methods.clone(),
59                    prev: op.prev.clone(),
60                })
61            }
62            _ => unreachable!("PLC current state should always be an UnsignedPLC")
63        }
64    }
65
66    pub fn with_key(&mut self, key: &'k Keypair) -> &mut Self {
67        self.key = Some(key);
68        self
69    }
70
71    pub fn with_validation_key(&mut self, key: &Keypair) -> &mut Self {
72        self.verification_methods
73            .insert("atproto".to_string(), key.to_did_key().unwrap());
74        self
75    }
76
77    pub fn with_handle(&mut self, handle: String) -> &mut Self {
78        self.also_known_as.push(assure_at_prefix(&handle));
79        self
80    }
81
82    pub fn with_pds(&mut self, pds: String) -> &mut Self {
83        self.services.insert(
84            "atproto_pds".to_string(),
85            Service {
86                type_: "AtprotoPersonalDataServer".to_string(),
87                endpoint: assure_http(&pds),
88            },
89        );
90        self
91    }
92
93    pub fn add_rotation_key(&mut self, key: &Keypair) -> &mut Self {
94        self.rotation_keys.push(key.to_did_key().unwrap());
95        self
96    }
97
98    pub fn add_known_as(&mut self, name: String) -> &mut Self {
99        self.also_known_as.push(name);
100        self
101    }
102
103    pub fn set_prev(&mut self, prev: String) -> &mut Self {
104        self.prev = Some(prev);
105        self
106    }
107
108    pub async fn build(&mut self, op_type: PLCOperationType) -> Result<SignedPLCOperation, PLCError> {
109        if op_type == PLCOperationType::Operation {
110            // These fields only apply to operations, not tombstone operations
111            if self.services.get("atproto_pds").is_none() {
112                return Err(PLCError::InvalidOperation)
113            }
114            if self.key.is_none() {
115                return Err(PLCError::InvalidOperation)
116            }
117            if self.rotation_keys.len() < 2 {
118                return Err(PLCError::InvalidOperation)
119            }
120            if self.also_known_as.len() < 1 {
121                return Err(PLCError::InvalidOperation)
122            }
123            if self.verification_methods.get("atproto").is_none() {
124                return Err(PLCError::InvalidOperation)
125            }
126        }
127        if self.did.is_some() {
128            // Not a genesis op
129            match &self.prev {
130                Some(_) => (),
131                None => {
132                    // Try and automatically retreive previous log CID
133                    let audit_log = self.plc.get_audit_log(&self.did.as_ref().unwrap()).await?;
134                    self.set_prev(audit_log.get_latest()?);
135                    ()
136                }
137            }
138        }
139        let op = UnsignedPLCOperation {
140            type_: op_type,
141            verification_methods: self.verification_methods.clone(),
142            services: self.services.clone(),
143            rotation_keys: self.rotation_keys.clone(),
144            also_known_as: self.also_known_as.clone(),
145            prev: self.prev.clone(),
146        };
147        let key = &self
148            .key
149            .clone()
150            .unwrap()
151            .to_private_key()
152            .map_err(|e| PLCError::Other(e.into()))?;
153        op.to_signed(key.as_str()).map_err(|e| PLCError::Other(e.into()))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    const PLC_HOST: &str = "https://plc.directory";
162
163    #[actix_rt::test]
164    async fn test_operation_builder() {
165        let plc = DIDPLC::new(PLC_HOST);
166    
167        let signing_key = Keypair::generate(crate::BlessedAlgorithm::K256);
168        let recovery_key = Keypair::generate(crate::BlessedAlgorithm::K256);
169        let validation_key = Keypair::generate(crate::BlessedAlgorithm::K256);
170    
171        let mut builder = OperationBuilder::new(&plc);
172        builder.with_key(&signing_key);
173        builder.with_validation_key(&validation_key);
174        builder.with_handle("example.test".to_string());
175        builder.with_pds("https://example.test".to_string());
176        builder.add_rotation_key(&recovery_key);
177        builder.add_rotation_key(&signing_key);
178        
179        let op = builder.build(PLCOperationType::Operation).await;
180        assert!(op.is_ok(), "Operation should build");
181    }
182
183    #[actix_rt::test]
184    async fn test_from_did_state() {
185        let plc = DIDPLC::new(PLC_HOST);
186        let did = "did:plc:z72i7hdynmk6r22z27h6tvur".to_string();
187        let builder = OperationBuilder::from_did_state(&plc, did).await;
188        assert!(builder.is_ok(), "Operation builder should create: {:?}", builder.err().unwrap());
189    }
190}