Skip to main content

fatline_rs/
lib.rs

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
24// Simplified hub service type
25pub type HubService = proto::hub_service_client::HubServiceClient<Channel>;
26
27// Re-export ed25519 constants so they're available for services
28pub const SIGNATURE_LENGTH: usize = ed25519_dalek::SIGNATURE_LENGTH;
29pub const PUBLIC_KEY_LENGTH: usize = ed25519_dalek::PUBLIC_KEY_LENGTH;
30
31// 160-bit blake3 hash length used by Farcaster rpc requests
32pub const HASH_LENGTH: usize = 20;
33// Farcaster epoch in seconds, used to calculate timestamps in rpc
34pub const FARCASTER_EPOCH: u64 = 1609459200;
35
36pub mod proto {
37    use tonic::{IntoRequest, Request};
38
39    tonic::include_proto!("_"); // farcaster protos don't use package
40
41    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            // map the inner data
78            .and_then(|data| data.body)
79            // map the inner UserDataBody
80            .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                // transform optional and vecs to fatline types
189                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                // copy text out
227                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}