fatline-rs 0.2.2

Farcaster grpc client and utility library
Documentation
use crate::proto::{FarcasterNetwork, HashScheme, Message, MessageData, MessageType, SignatureScheme, UserDataBody, UserDataType};
use crate::to_messages::ToMessages;
use crate::utils::{message_hash, sign_hash, FieldUpdate};
use derive_builder::Builder;
use ed25519_dalek::{SigningKey, VerifyingKey};
use crate::proto::message_data::Body;
use crate::utils;

#[derive(Builder, Default, Clone)]
#[builder(setter(strip_option), default)]
pub struct ProfileUpdate {
    username_update: Option<FieldUpdate<String>>,
    display_name_update: Option<FieldUpdate<String>>,
    profile_pic_update: Option<FieldUpdate<String>>,
    bio_update: Option<FieldUpdate<String>>,
    url_update: Option<FieldUpdate<String>>,
    location_update: Option<FieldUpdate<(f32,f32)>>,
    twitter_update: Option<FieldUpdate<String>>,
    github_update: Option<FieldUpdate<String>>,
}

fn field_update_to_message(fid: u64, timestamp: u32, field_update: impl Into<String>, user_data_type: UserDataType) -> MessageData {
    MessageData {
        r#type: MessageType::UserDataAdd.into(),
        fid,
        timestamp,
        network: FarcasterNetwork::Mainnet.into(),
        body: Some(Body::UserDataBody(UserDataBody {
            r#type: user_data_type.into(),
            value: field_update.into(),
        })),
    }
}

impl Into<String> for FieldUpdate<String> {
    fn into(self) -> String {
        if let FieldUpdate::Add(value) = self {
            value
        } else {
            String::new()
        }
    }
}

impl ToMessages for ProfileUpdate {
    fn to_messages(self, fid: u64, signing_key: &SigningKey) -> Vec<Message> {
        let current_time = utils::now();

        let mut messages = Vec::new();

        if let Some(username_update) = self.username_update {
            messages.push(field_update_to_message(fid, current_time, username_update, UserDataType::Username));
        }

        if let Some(display_name_update) = self.display_name_update {
            messages.push(field_update_to_message(fid, current_time, display_name_update, UserDataType::Display));
        }

        if let Some(profile_pic_update) = self.profile_pic_update {
            messages.push(field_update_to_message(fid, current_time, profile_pic_update, UserDataType::Pfp));
        }

        if let Some(bio_update) = self.bio_update {
            messages.push(field_update_to_message(fid, current_time, bio_update, UserDataType::Bio));
        }

        if let Some(url_update) = self.url_update {
            messages.push(field_update_to_message(fid, current_time, url_update, UserDataType::Url));
        }

        if let Some(location_update) = self.location_update {
            let encoded = if let FieldUpdate::Add((lat,long)) = location_update {
                format!("geo:{:.2},{:.2}",lat,long)
            } else {
                String::new()
            };
            messages.push(field_update_to_message(fid, current_time, encoded, UserDataType::Location));
        }

        if let Some(twitter_update) = self.twitter_update {
            messages.push(field_update_to_message(fid, current_time, twitter_update, UserDataType::Twitter));
        }

        if let Some(github_update) = self.github_update {
            messages.push(field_update_to_message(fid, current_time, github_update, UserDataType::Github));
        }

        messages.iter()
            .cloned()
            .map(|data| {
                let hash = message_hash(&data);
                let signature = sign_hash(signing_key, &hash);
                Message {
                    data: Some(data),
                    hash: hash.to_vec(),
                    hash_scheme: HashScheme::Blake3.into(),
                    signature: signature.to_vec(),
                    signature_scheme: SignatureScheme::Ed25519.into(),
                    signer: VerifyingKey::from(signing_key).as_bytes().to_vec(),
                    data_bytes: None,
                }
            }).collect()
    }
}

#[cfg(test)]
mod tests {
    use ed25519_dalek::{Signature, SigningKey, Verifier};
    use rand::rngs::OsRng;
    use crate::profile_builder::{ProfileUpdateBuilder};
    use crate::proto::MessageType;
    use crate::utils::FieldUpdate;
    use crate::to_messages::ToMessages;

    #[tokio::test]
    async fn test_expected_username_update() -> eyre::Result<()> {
        let update = ProfileUpdateBuilder::create_empty()
            .username_update(FieldUpdate::Add("username".to_string()))
            .display_name_update(FieldUpdate::Add("Username".to_string()))
            .bio_update(FieldUpdate::Remove)
            .build()?;

        let signing_key = SigningKey::generate(&mut OsRng);

        let messages = update.to_messages(0, &signing_key);
        
        let verifying_key = signing_key.verifying_key();
        assert_eq!(3, messages.len());
        for message in &messages {
            assert_eq!(message.signer, verifying_key.as_bytes());
            assert!(verifying_key.verify(&message.hash, &Signature::from_slice(&message.signature)?).is_ok());
        }
        assert_eq!(messages.first().unwrap().data.clone().unwrap().r#type(), MessageType::UserDataAdd);

        Ok(())
    }

}