heddle_thread_api/
creation.rs1use api::v2::client::{ClientError, RpcTransport};
4use crypto::Signer;
5use heddle_object_model::object::{OperationId, thread_replication::ThreadGenesis};
6
7use crate::{Remote, contract::*, replication::opening, rpc, transport::Error};
8
9pub struct ThreadCreation {
10 request: StartThreadRequest,
11 reference: ThreadRef,
12}
13
14impl ThreadCreation {
15 pub fn sign(
16 operation_id: impl Into<String>,
17 genesis: &ThreadGenesis,
18 signer: &impl Signer,
19 ) -> Result<Self, Error> {
20 let operation_id = operation_id.into();
21 validate_operation(&operation_id)?;
22 Self::from_signed(operation_id, opening::sign_genesis(genesis, signer)?)
23 }
24
25 pub fn sign_with_authority(
26 operation_id: impl Into<String>,
27 genesis: &ThreadGenesis,
28 signer: &impl Signer,
29 creator_authority: Vec<u8>,
30 ) -> Result<Self, Error> {
31 Self::from_signed_with_authority(
32 operation_id,
33 opening::sign_genesis(genesis, signer)?,
34 creator_authority,
35 )
36 }
37
38 pub fn from_signed(
40 operation_id: impl Into<String>,
41 record: SignedRecord,
42 ) -> Result<Self, Error> {
43 Self::from_signed_with_authority(operation_id, record, Vec::new())
44 }
45
46 pub fn from_signed_with_authority(
49 operation_id: impl Into<String>,
50 record: SignedRecord,
51 creator_authority: Vec<u8>,
52 ) -> Result<Self, Error> {
53 let operation_id = operation_id.into();
54 validate_operation(&operation_id)?;
55 let genesis = ThreadGenesis::decode(&record.canonical_record)
56 .map_err(|_| Error::Protocol("invalid canonical Thread genesis"))?;
57 use heddle_object_model::object::thread_replication::GenesisOwner;
58 match &genesis.owner {
59 GenesisOwner::LocalKey(_) if !creator_authority.is_empty() => {
60 return Err(Error::Protocol(
61 "local-key ownership requires an explicit claim, not an account proof on upload",
62 ));
63 }
64 GenesisOwner::Account(_) if creator_authority.is_empty() => {
65 return Err(Error::Protocol(
66 "account-owned genesis requires original creator authority",
67 ));
68 }
69 _ => {}
70 }
71 if creator_authority.len() > 64 * 1024 {
72 return Err(Error::Protocol("creator authority exceeds bound"));
73 }
74 let reference = ThreadRef {
75 spool: Some(SpoolRef {
76 id: genesis.spool.clone(),
77 }),
78 id: Some(ThreadId {
79 value: genesis
80 .id()
81 .map_err(|_| Error::Protocol("invalid Thread identity"))?
82 .as_bytes()
83 .to_vec(),
84 }),
85 };
86 opening::verify_genesis(&record, &reference)?;
87 Ok(Self {
88 request: StartThreadRequest {
89 client_operation_id: operation_id,
90 spool: reference.spool.clone(),
91 thread_genesis: Some(record),
92 creator_authority,
93 },
94 reference,
95 })
96 }
97
98 pub fn request(&self) -> &StartThreadRequest {
99 &self.request
100 }
101 pub fn reference(&self) -> &ThreadRef {
102 &self.reference
103 }
104 pub fn genesis_record(&self) -> ThreadGenesisRecord {
105 ThreadGenesisRecord {
106 boundary_acceptances: Vec::new(),
107 ownership_claims: vec![],
108 ownership_claim_admissions: vec![],
109 ownership_resolutions: vec![],
110 ownership_resolution_admissions: vec![],
111 genesis: self.request.thread_genesis.clone(),
112 creator_authority: self.request.creator_authority.clone(),
113 admission: None,
114 }
115 }
116}
117
118fn validate_operation(operation_id: &str) -> Result<(), Error> {
119 operation_id
120 .parse::<OperationId>()
121 .map(|_| ())
122 .map_err(|_| Error::Protocol("client operation ID must be a UUID"))
123}
124
125impl<T: RpcTransport<Error = Error>> Remote<T> {
126 pub async fn start_thread(
129 &self,
130 creation: &ThreadCreation,
131 ) -> Result<ThreadMutationResponse, ClientError<Error>> {
132 if !self
133 .description
134 .understood_signed_record_formats
135 .iter()
136 .any(|format| format == heddle_object_model::object::thread_replication::GENESIS_FORMAT)
137 {
138 return Err(ClientError::Transport(Error::Protocol(
139 "endpoint does not understand Thread genesis",
140 )));
141 }
142 let response = self
143 .api
144 .call::<rpc::ThreadServiceStartThread>(creation.request())
145 .await?;
146 let receipt = response
147 .receipt
148 .as_ref()
149 .ok_or(ClientError::Transport(Error::Protocol(
150 "Thread creation response has no receipt",
151 )))?;
152 if receipt.client_operation_id != creation.request.client_operation_id
153 || receipt.endpoint != self.description.endpoint
154 || receipt.outcome.is_none()
155 || response
156 .thread
157 .as_ref()
158 .is_some_and(|thread| thread.r#ref.as_ref() != Some(creation.reference()))
159 || (matches!(receipt.outcome, Some(mutation_receipt::Outcome::Applied(_)))
160 && response.thread.is_none())
161 {
162 return Err(ClientError::Transport(Error::Protocol(
163 "Thread creation response does not match the command",
164 )));
165 }
166 Ok(response)
167 }
168}