1pub use ed25519::{SigningKey, VerifyingKey};
2use ed25519_dalek as ed25519;
3pub use prost::Message as MessageTrait;
4use tonic::transport::Channel;
5
6#[cfg(feature = "subscription")]
7pub mod sub;
8
9pub mod action;
10mod error;
11
12pub mod cast_builder;
13#[cfg(feature = "service_types")]
14pub mod hub;
15#[cfg(feature = "service_types")]
16pub mod posts;
17pub mod profile_builder;
18#[cfg(feature = "service_types")]
19pub mod stream;
20pub mod to_messages;
21#[cfg(feature = "service_types")]
22pub mod users;
23
24pub type HubService = proto::hub_service_client::HubServiceClient<Channel>;
26
27pub const SIGNATURE_LENGTH: usize = ed25519_dalek::SIGNATURE_LENGTH;
29pub const PUBLIC_KEY_LENGTH: usize = ed25519_dalek::PUBLIC_KEY_LENGTH;
30
31pub const HASH_LENGTH: usize = 20;
33pub const FARCASTER_EPOCH: u64 = 1609459200;
35
36pub mod proto {
37 use tonic::{IntoRequest, Request};
38
39 tonic::include_proto!("_"); impl IntoRequest<SubmitBulkMessagesRequest> for Vec<Message> {
42 fn into_request(self) -> Request<SubmitBulkMessagesRequest> {
43 Request::new(SubmitBulkMessagesRequest { messages: self })
44 }
45 }
46}
47
48pub mod utils {
49 use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH};
50
51 use crate::proto::{Message as FMessage, VerificationAddAddressBody};
52 use ed25519_dalek::{Signature, Signer, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH};
53 use eyre::Result;
54 use prost::Message;
55 use rand::rngs::OsRng;
56 use tonic::Response;
57
58 use super::ed25519::SigningKey;
59 use crate::action::*;
60 use crate::posts::{Cast, CastId, Embed, Mention, Parent};
61 use crate::proto::cast_add_body::Parent as PParent;
62 use crate::proto::embed::Embed as PEmbed;
63 use crate::proto::link_body::Target::TargetFid;
64 use crate::proto::message_data::Body;
65 use crate::proto::reaction_body::Target::TargetCastId;
66 use crate::proto::{LinkBody, MessageData, MessageType, ReactionBody, UserDataBody};
67 use crate::{FARCASTER_EPOCH, HASH_LENGTH};
68
69 pub fn generate_signing_key() -> SigningKey {
70 SigningKey::generate(&mut OsRng)
71 }
72
73 pub fn optional_get_user_data(response: Response<FMessage>) -> Option<UserDataBody> {
74 response
75 .into_inner()
76 .data
77 .and_then(|data| data.body)
79 .and_then(|body| match body {
81 Body::UserDataBody(body) => Some(body),
82 _ => None,
83 })
84 }
85
86 pub fn optional_get_user_data_value(user_data_body: UserDataBody) -> Option<String> {
87 Some(user_data_body.value)
88 }
89
90 pub fn optional_get_reaction_message(response: Response<FMessage>) -> Option<ReactionBody> {
91 response
92 .into_inner()
93 .data
94 .and_then(|data| data.body)
95 .and_then(|body| match body {
96 Body::ReactionBody(body) => Some(body),
97 _ => None,
98 })
99 }
100
101 pub fn optional_get_follow_message(response: Response<FMessage>) -> Option<LinkBody> {
102 response
103 .into_inner()
104 .data
105 .and_then(|data| data.body)
106 .and_then(|body| match body {
107 Body::LinkBody(body) => Some(body),
108 _ => None,
109 })
110 }
111
112 pub fn truncated_hash(bytes: &[u8]) -> [u8; HASH_LENGTH] {
113 let hash = blake3::hash(&bytes);
114 let mut truncated: [u8; HASH_LENGTH] = [0u8; HASH_LENGTH];
115 truncated.copy_from_slice(&hash.as_bytes()[0..HASH_LENGTH]);
116 truncated
117 }
118
119 pub fn message_hash(message_data: &MessageData) -> [u8; HASH_LENGTH] {
120 let hash = blake3::hash(&message_data.encode_to_vec());
121 let mut truncated: [u8; HASH_LENGTH] = [0u8; HASH_LENGTH];
122 truncated.copy_from_slice(&hash.as_bytes()[0..HASH_LENGTH]);
123 truncated
124 }
125
126 pub fn sign_hash(signing_key: &SigningKey, hash: &[u8; HASH_LENGTH]) -> [u8; SIGNATURE_LENGTH] {
127 signing_key.sign(hash).to_bytes()
128 }
129
130 pub fn now() -> u32 {
131 (SystemTime::now()
132 .duration_since(UNIX_EPOCH)
133 .unwrap()
134 .as_secs()
135 - FARCASTER_EPOCH) as u32
136 }
137
138 pub fn fc_timestamp_to_unix(fc_epoch_time_seconds: u32) -> Result<u32, SystemTimeError> {
139 Ok((UNIX_EPOCH
140 + Duration::from_secs(FARCASTER_EPOCH)
141 + Duration::from_secs(fc_epoch_time_seconds as u64))
142 .duration_since(UNIX_EPOCH)?
143 .as_secs() as u32)
144 }
145
146 pub fn validate_signed_by(
147 msg: &[u8; HASH_LENGTH],
148 sig: &[u8; SIGNATURE_LENGTH],
149 pub_key: &[u8; PUBLIC_KEY_LENGTH],
150 ) -> Result<bool> {
151 let signature = Signature::from_slice(sig)?;
152
153 let verifying_key = VerifyingKey::from_bytes(pub_key)?;
154
155 let verify_result = verifying_key.verify_strict(msg, &signature);
156 if verify_result.is_ok() {
157 Ok(true)
158 } else {
159 Ok(false)
160 }
161 }
162
163 pub struct NonExistentTypeError;
164
165 impl TryInto<MessageType> for String {
166 type Error = NonExistentTypeError;
167
168 fn try_into(self) -> std::result::Result<MessageType, Self::Error> {
169 let link_add_str = MessageType::LinkAdd.as_str_name().to_string();
170 let link_rem_str = MessageType::LinkRemove.as_str_name().to_string();
171 if self == link_add_str {
172 Ok(MessageType::LinkAdd)
173 } else if self == link_rem_str {
174 Ok(MessageType::LinkRemove)
175 } else {
176 Err(NonExistentTypeError)
177 }
178 }
179 }
180
181 pub fn cast_action_from_message(message: crate::proto::Message) -> Option<CastAction> {
182 let data = message.data?;
183 let author = data.fid;
184 let hash = message.hash.clone();
185
186 match data.body? {
187 Body::CastAddBody(add) => {
188 let embeds = add
190 .embeds
191 .iter()
192 .cloned()
193 .filter_map(|e| {
194 e.embed.map(|e| match e {
195 PEmbed::Url(url) => Embed::Url(url),
196 PEmbed::CastId(cast) => Embed::CastId(CastId {
197 fid: cast.fid,
198 hash: cast.hash,
199 }),
200 })
201 })
202 .collect::<Vec<Embed>>();
203
204 let parent = add.parent.map(|p| match p {
205 PParent::ParentUrl(url) => Parent::Url(url),
206 PParent::ParentCastId(cast) => Parent::CastId(CastId {
207 fid: cast.fid,
208 hash: cast.hash,
209 }),
210 });
211
212 let mentions = add
213 .mentions
214 .iter()
215 .copied()
216 .enumerate()
217 .filter_map(|(pos, fid)| {
218 let start = add.mentions_positions.get(pos)?.to_owned();
219 Some(Mention {
220 fid,
221 start_pos: start,
222 })
223 })
224 .collect();
225
226 let text = add.text;
228 Some(CastAction::CastAdd(Cast {
229 cast_id: CastId { fid: author, hash },
230 text,
231 mentions,
232 embeds,
233 parent,
234 cast_time: data.timestamp,
235 }))
236 }
237 Body::CastRemoveBody(remove) => Some(CastAction::CastRemove(CastId {
238 fid: author,
239 hash: remove.target_hash,
240 })),
241 _ => None,
242 }
243 }
244
245 pub fn link_from_message(message: FMessage) -> Option<Vec<LinkAction>> {
246 let data = message.data?;
247 let data_type = data.r#type();
248
249 match data.body? {
250 Body::LinkBody(link) => {
251 if link.r#type != "follow" {
252 return None;
253 }
254 let TargetFid(target) = link.target?;
255 let mapped = map_link_action(&data_type, data.fid, target, data.timestamp)?;
256 Some(vec![mapped])
257 }
258 Body::LinkCompactStateBody(compaction) => {
259 let mapped = compaction
260 .target_fids
261 .iter()
262 .copied()
263 .filter_map(|target| {
264 map_link_action(&data_type, data.fid, target, data.timestamp)
265 })
266 .collect::<Vec<_>>();
267 if mapped.is_empty() {
268 None
269 } else {
270 Some(mapped)
271 }
272 }
273 _ => None,
274 }
275 }
276
277 pub fn verification_from_message(message: FMessage) -> Option<VerificationAddAddressBody> {
278 let data = message.data?;
279 match data.body? {
280 Body::VerificationAddAddressBody(verification) => Some(verification),
281 _ => None,
282 }
283 }
284
285 pub fn reaction_from_message(message: FMessage) -> Option<ReactionAction> {
286 let data = message.data?;
287 let data_type = data.r#type();
288
289 match data.body? {
290 Body::ReactionBody(reaction) => {
291 let reaction_info = match reaction.target? {
292 TargetCastId(cast_id) => Some(ReactionInfo {
293 reference: CastId {
294 hash: cast_id.hash,
295 fid: cast_id.fid,
296 },
297 timestamp: data.timestamp,
298 reaction_type: reaction.r#type,
299 }),
300 _ => None,
301 }?;
302 let is_add = match data_type {
303 MessageType::ReactionAdd => true,
304 MessageType::ReactionRemove => false,
305 _ => return None,
306 };
307 if is_add {
308 Some(ReactionAction::Add(reaction_info))
309 } else {
310 Some(ReactionAction::Remove(reaction_info))
311 }
312 }
313 _ => None,
314 }
315 }
316
317 fn map_link_action(
318 message_type: &MessageType,
319 source: u64,
320 target: u64,
321 timestamp: u32,
322 ) -> Option<LinkAction> {
323 let info = LinkInfo {
324 source_fid: source,
325 target_fid: target,
326 timestamp,
327 };
328 match message_type {
329 MessageType::LinkAdd => Some(LinkAction::AddFollow(info)),
330 MessageType::LinkRemove => Some(LinkAction::RemoveFollow(info)),
331 MessageType::LinkCompactState => Some(LinkAction::AddFollow(info)),
332 _ => None,
333 }
334 }
335
336 #[derive(Debug, Eq, PartialEq, Clone)]
337 pub enum FieldUpdate<T> {
338 Add(T),
339 Remove,
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use crate::hub::HubInfoService;
346 use crate::proto::{
347 FidRequest, MessageData, Protocol,
348 };
349 use crate::utils::verification_from_message;
350 use crate::HubService;
351 use prost::Message;
352 use tonic::transport::Uri;
353
354 #[tokio::test]
355 async fn test_ok() -> Result<(), Box<dyn std::error::Error>> {
356 let data = MessageData {
357 r#type: 0,
358 fid: 0,
359 timestamp: 0,
360 network: 0,
361 body: None,
362 };
363
364 data.encode_to_vec();
365
366 Ok(())
367 }
368
369 #[tokio::test]
370 async fn connect_hub() -> eyre::Result<()> {
371 let hub_url = dotenvy::var("HUB_URL")?;
372 let uri = hub_url.parse::<Uri>()?;
373 let mut hub = HubService::connect(uri).await?;
374 let total_count = hub.get_current_fid_count().await?;
375 assert!(total_count > 0);
376 Ok(())
377 }
378
379 #[tokio::test]
380 async fn number_solana_accts() -> eyre::Result<()> {
381 let hub_url = dotenvy::var("HUB_URL")?;
382 let uri = hub_url.parse::<Uri>()?;
383 let mut hub = HubService::connect(uri).await?;
384 let total_count = hub.get_current_fid_count().await?;
385
386 let mut num_sol = 0;
387 let mut num_users = 0;
388
389 for fid in 1..=total_count {
390 let req = FidRequest {
391 fid,
392 page_size: Some(1000),
393 page_token: None,
394 reverse: None,
395 };
396
397 let result = hub.get_verifications_by_fid(req).await?.into_inner();
398
399 let mut has_sol = false;
400
401 result
402 .messages
403 .iter()
404 .cloned()
405 .filter_map(|m| verification_from_message(m))
406 .for_each(|v| {
407 if v.protocol() == Protocol::Solana {
408 num_sol += 1;
409 has_sol = true;
410 }
411 });
412 if has_sol {
413 num_users += 1;
414 }
415
416 if fid % 20_000 == 0 {
417 println!("visited {fid}, num_sol: {num_sol}, num_users: {num_users}");
418 }
419 }
420
421 println!("num sol: {num_sol}, num_users: {num_users}");
422
423 Ok(())
424 }
425}