did_method_plc/
op_builder.rs1use 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 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 match &self.prev {
130 Some(_) => (),
131 None => {
132 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}