fplus_lib/core/
mod.rs

1use std::str::FromStr;
2
3use futures::future;
4use octocrab::models::{
5    pulls::PullRequest,
6    repos::{Content, ContentItems},
7};
8use reqwest::Response;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    base64,
13    config::get_env_var_or_default,
14    error::LDNError,
15    external_services::github::{
16        CreateMergeRequestData, CreateRefillMergeRequestData, GithubWrapper,
17    },
18    parsers::ParsedIssue,
19};
20
21use self::application::file::{
22    AllocationRequest, AllocationRequestType, AppState, ApplicationFile, NotaryInput,
23    ValidNotaryList, ValidRKHList,
24};
25use rayon::prelude::*;
26
27pub mod application;
28
29const DEV_BOT_USER: &str = "filplus-github-bot-read-write[bot]";
30const PROD_BOT_USER: &str = "filplus-falcon[bot]";
31
32#[derive(Deserialize)]
33pub struct CreateApplicationInfo {
34    pub issue_number: String,
35}
36
37#[derive(Deserialize, Serialize, Debug)]
38pub struct NotaryList(pub Vec<String>);
39
40#[derive(Deserialize, Serialize, Debug)]
41pub struct CompleteNewApplicationProposalInfo {
42    signer: NotaryInput,
43    request_id: String,
44}
45
46#[derive(Debug)]
47pub struct LDNApplication {
48    github: GithubWrapper,
49    pub application_id: String,
50    pub file_sha: String,
51    pub file_name: String,
52    pub branch_name: String,
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56pub struct CompleteGovernanceReviewInfo {
57    actor: String,
58}
59
60#[derive(Deserialize, Debug)]
61pub struct RefillInfo {
62    pub id: String,
63    pub amount: String,
64    pub amount_type: String,
65}
66
67#[derive(Deserialize)]
68pub struct ValidationPullRequestData {
69    pub pr_number: String,
70    pub user_handle: String,
71}
72
73#[derive(Deserialize)]
74pub struct ValidationIssueData {
75    pub issue_number: String,
76    pub user_handle: String,
77}
78
79impl LDNApplication {
80    pub async fn single_active(pr_number: u64) -> Result<ApplicationFile, LDNError> {
81        let gh: GithubWrapper = GithubWrapper::new();
82        let (_, pull_request) = gh.get_pull_request_files(pr_number).await.unwrap();
83        let pull_request = pull_request.get(0).unwrap();
84        let pull_request: Response = reqwest::Client::new()
85            .get(&pull_request.raw_url.to_string())
86            .send()
87            .await
88            .map_err(|e| LDNError::Load(format!("Failed to get pull request files /// {}", e)))?;
89        let pull_request = pull_request
90            .text()
91            .await
92            .map_err(|e| LDNError::Load(format!("Failed to get pull request files /// {}", e)))?;
93        if let Ok(app) = serde_json::from_str::<ApplicationFile>(&pull_request) {
94            Ok(app)
95        } else {
96            Err(LDNError::Load(format!(
97                "Pull Request {} Application file is corrupted",
98                pr_number
99            )))
100        }
101    }
102
103    async fn load_pr_files(
104        pr: PullRequest,
105    ) -> Result<Option<(String, String, ApplicationFile, PullRequest)>, LDNError> {
106        let gh = GithubWrapper::new();
107        let files = match gh.get_pull_request_files(pr.number).await {
108            Ok(files) => files,
109            Err(_) => return Ok(None),
110        };
111        let raw_url = match files.1.get(0) {
112            Some(f) => f.raw_url.clone(),
113            None => return Ok(None),
114        };
115        let response = reqwest::Client::new().get(raw_url).send().await;
116        let response = match response {
117            Ok(response) => response,
118            Err(_) => return Ok(None),
119        };
120        let response = response.text().await;
121        let response = match response {
122            Ok(response) => response,
123            Err(_) => return Ok(None),
124        };
125        let app = match ApplicationFile::from_str(&response) {
126            Ok(app) => app,
127            Err(e) => {
128                dbg!(&e);
129                return Ok(None);
130            }
131        };
132        Ok(Some((
133            files.1.get(0).unwrap().sha.clone(),
134            files.1.get(0).unwrap().filename.clone(),
135            app,
136            pr.clone(),
137        )))
138    }
139
140    pub async fn load(application_id: String) -> Result<Self, LDNError> {
141        let gh: GithubWrapper = GithubWrapper::new();
142        let pull_requests = gh.list_pull_requests().await.unwrap();
143        let pull_requests = future::try_join_all(
144            pull_requests
145                .into_iter()
146                .map(|pr: PullRequest| (LDNApplication::load_pr_files(pr)))
147                .collect::<Vec<_>>(),
148        )
149        .await?;
150        let result = pull_requests
151            .par_iter()
152            .filter(|pr| {
153                if let Some(r) = pr {
154                    if String::from(r.2.id.clone()) == application_id.clone() {
155                        return true;
156                    } else {
157                        return false;
158                    }
159                } else {
160                    return false;
161                }
162            })
163            .collect::<Vec<_>>();
164        if let Some(r) = result.get(0) {
165            if let Some(r) = r {
166                return Ok(Self {
167                    github: gh,
168                    application_id: r.2.id.clone(),
169                    file_sha: r.0.clone(),
170                    file_name: r.1.clone(),
171                    branch_name: r.3.head.ref_field.clone(),
172                });
173            }
174        }
175
176        let app = Self::single_merged(application_id).await?;
177        return Ok(Self {
178            github: gh,
179            application_id: app.1.id.clone(),
180            file_sha: app.0.sha.clone(),
181            file_name: app.0.path.clone(),
182            branch_name: "main".to_string(),
183        });
184    }
185
186    pub async fn active(filter: Option<String>) -> Result<Vec<ApplicationFile>, LDNError> {
187        let gh: GithubWrapper = GithubWrapper::new();
188        let mut apps: Vec<ApplicationFile> = Vec::new();
189        let pull_requests = gh.list_pull_requests().await.unwrap();
190        let pull_requests = future::try_join_all(
191            pull_requests
192                .into_iter()
193                .map(|pr: PullRequest| LDNApplication::load_pr_files(pr))
194                .collect::<Vec<_>>(),
195        )
196        .await
197        .unwrap();
198        for r in pull_requests {
199            if r.is_some() {
200                let r = r.unwrap();
201                if filter.is_none() {
202                    apps.push(r.2)
203                } else {
204                    if r.2.id == filter.clone().unwrap() {
205                        apps.push(r.2)
206                    }
207                }
208            }
209        }
210        Ok(apps)
211    }
212
213    /// Create New Application
214    pub async fn new_from_issue(info: CreateApplicationInfo) -> Result<Self, LDNError> {
215        let issue_number = info.issue_number;
216        let gh: GithubWrapper = GithubWrapper::new();
217        let (parsed_ldn, _) = LDNApplication::parse_application_issue(issue_number.clone()).await?;
218        let application_id = parsed_ldn.id.clone();
219        let file_name = LDNPullRequest::application_path(&application_id);
220        let branch_name = LDNPullRequest::application_branch_name(&application_id);
221
222        match gh.get_file(&file_name, &branch_name).await {
223            Err(_) => {
224                let application_file = ApplicationFile::new(
225                    issue_number.clone(),
226                    "MULTISIG ADDRESS".to_string(),
227                    parsed_ldn.version,
228                    parsed_ldn.id.clone(),
229                    parsed_ldn.client.clone(),
230                    parsed_ldn.project,
231                    parsed_ldn.datacap,
232                )
233                .await;
234                let file_content = match serde_json::to_string_pretty(&application_file) {
235                    Ok(f) => f,
236                    Err(e) => {
237                        return Err(LDNError::New(format!(
238                            "Application issue file is corrupted /// {}",
239                            e
240                        )))
241                    }
242                };
243                let app_id = parsed_ldn.id.clone();
244                let file_sha = LDNPullRequest::create_pr(
245                    issue_number.clone(),
246                    parsed_ldn.client.name.clone(),
247                    branch_name.clone(),
248                    LDNPullRequest::application_path(&app_id),
249                    file_content.clone(),
250                )
251                .await?;
252                Self::issue_waiting_for_gov_review(issue_number.clone()).await?;
253                Ok(LDNApplication {
254                    github: gh,
255                    application_id,
256                    file_sha,
257                    file_name,
258                    branch_name,
259                })
260            }
261            Ok(_) => {
262                return Err(LDNError::New(format!(
263                    "Application issue {} already exists",
264                    application_id
265                )))
266            }
267        }
268    }
269
270    /// Move application from Governance Review to Proposal
271    pub async fn complete_governance_review(
272        &self,
273        info: CompleteGovernanceReviewInfo,
274    ) -> Result<ApplicationFile, LDNError> {
275        match self.app_state().await {
276            Ok(s) => match s {
277                AppState::Submitted => {
278                    let app_file: ApplicationFile = self.file().await?;
279                    let uuid = uuidv4::uuid::v4();
280                    let request = AllocationRequest::new(
281                        info.actor.clone(),
282                        uuid,
283                        AllocationRequestType::First,
284                        app_file.datacap.weekly_allocation.clone(),
285                    );
286                    let app_file = app_file.complete_governance_review(info.actor.clone(), request);
287                    let file_content = serde_json::to_string_pretty(&app_file).unwrap();
288                    let app_path = &self.file_name.clone();
289                    let app_branch = self.branch_name.clone();
290                    Self::issue_ready_to_sign(app_file.issue_number.clone()).await?;
291                    match LDNPullRequest::add_commit_to(
292                        app_path.to_string(),
293                        app_branch,
294                        LDNPullRequest::application_move_to_proposal_commit(&info.actor),
295                        file_content,
296                        self.file_sha.clone(),
297                    )
298                    .await
299                    {
300                        Some(()) => Ok(app_file),
301                        None => {
302                            return Err(LDNError::New(format!(
303                                "Application issue {} cannot be triggered(1)",
304                                self.application_id
305                            )))
306                        }
307                    }
308                }
309                _ => Err(LDNError::New(format!(
310                    "Application issue {} cannot be triggered(2)",
311                    self.application_id
312                ))),
313            },
314            Err(e) => Err(LDNError::New(format!(
315                "Application issue {} cannot be triggered {}(3)",
316                self.application_id, e
317            ))),
318        }
319    }
320
321    /// Move application from Proposal to Approved
322    pub async fn complete_new_application_proposal(
323        &self,
324        info: CompleteNewApplicationProposalInfo,
325    ) -> Result<ApplicationFile, LDNError> {
326        let CompleteNewApplicationProposalInfo { signer, request_id } = info;
327        match self.app_state().await {
328            Ok(s) => match s {
329                AppState::ReadyToSign => {
330                    let app_file: ApplicationFile = self.file().await?;
331                    if !app_file.allocation.is_active(request_id.clone()) {
332                        return Err(LDNError::Load(format!(
333                            "Request {} is not active",
334                            request_id
335                        )));
336                    }
337                    let app_lifecycle = app_file.lifecycle.finish_proposal();
338                    let app_file = app_file.add_signer_to_allocation(
339                        signer.clone().into(),
340                        request_id,
341                        app_lifecycle,
342                    );
343                    let file_content = serde_json::to_string_pretty(&app_file).unwrap();
344                    Self::issue_start_sign_dc(app_file.issue_number.clone()).await?;
345                    match LDNPullRequest::add_commit_to(
346                        self.file_name.to_string(),
347                        self.branch_name.clone(),
348                        LDNPullRequest::application_move_to_approval_commit(
349                            &signer.signing_address,
350                        ),
351                        file_content,
352                        self.file_sha.clone(),
353                    )
354                    .await
355                    {
356                        Some(()) => Ok(app_file),
357                        None => {
358                            return Err(LDNError::New(format!(
359                                "Application issue {} cannot be proposed(1)",
360                                self.application_id
361                            )))
362                        }
363                    }
364                }
365                _ => Err(LDNError::New(format!(
366                    "Application issue {} cannot be proposed(2)",
367                    self.application_id
368                ))),
369            },
370            Err(e) => Err(LDNError::New(format!(
371                "Application issue {} cannot be proposed {}(3)",
372                self.application_id, e
373            ))),
374        }
375    }
376
377    pub async fn complete_new_application_approval(
378        &self,
379        info: CompleteNewApplicationProposalInfo,
380    ) -> Result<ApplicationFile, LDNError> {
381        let CompleteNewApplicationProposalInfo { signer, request_id } = info;
382        match self.app_state().await {
383            Ok(s) => match s {
384                AppState::StartSignDatacap => {
385                    let app_file: ApplicationFile = self.file().await?;
386                    let app_lifecycle = app_file.lifecycle.finish_approval();
387                    let app_file = app_file.add_signer_to_allocation_and_complete(
388                        signer.clone().into(),
389                        request_id,
390                        app_lifecycle,
391                    );
392                    let file_content = serde_json::to_string_pretty(&app_file).unwrap();
393                    Self::issue_granted(app_file.issue_number.clone()).await?;
394                    match LDNPullRequest::add_commit_to(
395                        self.file_name.to_string(),
396                        self.branch_name.clone(),
397                        LDNPullRequest::application_move_to_confirmed_commit(
398                            &signer.signing_address,
399                        ),
400                        file_content,
401                        self.file_sha.clone(),
402                    )
403                    .await
404                    {
405                        Some(()) => Ok(app_file),
406                        None => {
407                            return Err(LDNError::New(format!(
408                                "Application issue {} cannot be proposed(1)",
409                                self.application_id
410                            )))
411                        }
412                    }
413                }
414                _ => Err(LDNError::New(format!(
415                    "Application issue {} cannot be proposed(2)",
416                    self.application_id
417                ))),
418            },
419            Err(e) => Err(LDNError::New(format!(
420                "Application issue {} cannot be proposed {}(3)",
421                self.application_id, e
422            ))),
423        }
424    }
425
426    async fn parse_application_issue(
427        issue_number: String,
428    ) -> Result<(ParsedIssue, String), LDNError> {
429        let gh: GithubWrapper = GithubWrapper::new();
430        let issue = gh
431            .list_issue(issue_number.parse().unwrap())
432            .await
433            .map_err(|e| {
434                LDNError::Load(format!(
435                    "Failed to retrieve issue {} from GitHub. Reason: {}",
436                    issue_number, e
437                ))
438            })?;
439        if let Some(issue_body) = issue.body {
440            Ok((ParsedIssue::from_issue_body(&issue_body), issue.user.login))
441        } else {
442            Err(LDNError::Load(format!(
443                "Failed to retrieve issue {} from GitHub. Reason: {}",
444                issue_number, "No body"
445            )))
446        }
447    }
448
449    /// Return Application state
450    async fn app_state(&self) -> Result<AppState, LDNError> {
451        let f = self.file().await?;
452        Ok(f.lifecycle.get_state())
453    }
454
455    /// Return Application state
456    pub async fn total_dc_reached(application_id: String) -> Result<bool, LDNError> {
457        let merged = Self::merged().await?;
458        let app = merged
459            .par_iter()
460            .find_first(|(_, app)| app.id == application_id);
461        if app.is_some() && app.unwrap().1.lifecycle.get_state() == AppState::Granted {
462            let app = app.unwrap().1.reached_total_datacap();
463            let gh: GithubWrapper = GithubWrapper::new();
464            let ldn_app = LDNApplication::load(application_id.clone()).await?;
465            let ContentItems { items } = gh.get_file(&ldn_app.file_name, "main").await.unwrap();
466            Self::issue_full_dc(app.issue_number.clone()).await?;
467            LDNPullRequest::create_refill_pr(
468                app.id.clone(),
469                app.client.name.clone(),
470                serde_json::to_string_pretty(&app).unwrap(),
471                ldn_app.file_name.clone(),
472                format!("{}-total-dc-reached", app.id),
473                items[0].sha.clone(),
474            )
475            .await?;
476            Ok(true)
477        } else {
478            return Err(LDNError::Load(format!(
479                "Application issue {} does not exist",
480                application_id
481            )));
482        }
483    }
484
485    fn content_items_to_app_file(file: ContentItems) -> Result<ApplicationFile, LDNError> {
486        let f = &file
487            .clone()
488            .take_items()
489            .get(0)
490            .and_then(|f| f.content.clone())
491            .and_then(|f| base64::decode(&f.replace("\n", "")))
492            .ok_or(LDNError::Load(format!("Application file is corrupted",)))?;
493        return Ok(ApplicationFile::from(f.clone()));
494    }
495
496    pub async fn file(&self) -> Result<ApplicationFile, LDNError> {
497        match self
498            .github
499            .get_file(&self.file_name, &self.branch_name)
500            .await
501        {
502            Ok(file) => {
503                return Ok(LDNApplication::content_items_to_app_file(file)?);
504            }
505            Err(e) => {
506                dbg!(&e);
507                return Err(LDNError::Load(format!(
508                    "Application issue {} file does not exist ///",
509                    self.application_id
510                )));
511            }
512        }
513    }
514
515    async fn fetch_noatries() -> Result<ValidNotaryList, LDNError> {
516        let gh = GithubWrapper::new();
517        let notaries = gh
518            .get_file("data/notaries.json", "main")
519            .await
520            .map_err(|e| LDNError::Load(format!("Failed to retrieve notaries /// {}", e)))?;
521
522        let notaries = &notaries.items[0]
523            .content
524            .clone()
525            .and_then(|f| base64::decode_notary(&f.replace("\n", "")))
526            .and_then(|f| Some(f));
527
528        if let Some(notaries) = notaries {
529            return Ok(notaries.clone());
530        } else {
531            return Err(LDNError::Load(format!("Failed to retrieve notaries ///")));
532        }
533    }
534
535    async fn fetch_rkh() -> Result<ValidRKHList, LDNError> {
536        let gh = GithubWrapper::new();
537        let rkh = gh
538            .get_file("data/rkh.json", "main")
539            .await
540            .map_err(|e| LDNError::Load(format!("Failed to retrieve rkh /// {}", e)))?;
541
542        let rkh = &rkh.items[0]
543            .content
544            .clone()
545            .and_then(|f| base64::decode_rkh(&f.replace("\n", "")))
546            .and_then(|f| Some(f));
547
548        if let Some(rkh) = rkh {
549            return Ok(rkh.clone());
550        } else {
551            return Err(LDNError::Load(format!("Failed to retrieve notaries ///")));
552        }
553    }
554
555    async fn single_merged(application_id: String) -> Result<(Content, ApplicationFile), LDNError> {
556        Ok(LDNApplication::merged()
557            .await?
558            .into_iter()
559            .find(|(_, app)| app.id == application_id)
560            .map_or_else(
561                || {
562                    return Err(LDNError::Load(format!(
563                        "Application issue {} does not exist",
564                        application_id
565                    )));
566                },
567                |app| Ok(app),
568            )?)
569    }
570
571    async fn map_merged(item: Content) -> Result<Option<(Content, ApplicationFile)>, LDNError> {
572        if item.download_url.is_none() {
573            return Ok(None);
574        }
575        let file = reqwest::Client::new()
576            .get(&item.download_url.clone().unwrap())
577            .send()
578            .await
579            .map_err(|e| LDNError::Load(format!("here {}", e)))?;
580        let file = file
581            .text()
582            .await
583            .map_err(|e| LDNError::Load(format!("here1 {}", e)))?;
584        let app = match ApplicationFile::from_str(&file) {
585            Ok(app) => {
586                if app.lifecycle.is_active {
587                    app
588                } else {
589                    return Ok(None);
590                }
591            }
592            Err(_) => {
593                return Ok(None);
594            }
595        };
596        Ok(Some((item, app)))
597    }
598
599    pub async fn merged() -> Result<Vec<(Content, ApplicationFile)>, LDNError> {
600        let gh = GithubWrapper::new();
601        let applications_path = "applications";
602        let mut all_files = gh.get_files(applications_path).await.map_err(|e| {
603            LDNError::Load(format!(
604                "Failed to retrieve all files from GitHub. Reason: {}",
605                e
606            ))
607        })?;
608        all_files
609            .items
610            .retain(|item| item.download_url.is_some() && item.name.ends_with(".json"));
611        let all_files = future::try_join_all(
612            all_files
613                .items
614                .into_iter()
615                .map(|fd| LDNApplication::map_merged(fd))
616                .collect::<Vec<_>>(),
617        )
618        .await
619        .map_err(|e| {
620            LDNError::Load(format!(
621                "Failed to fetch application files from their URLs. Reason: {}",
622                e
623            ))
624        })?;
625
626        let mut apps: Vec<(Content, ApplicationFile)> = vec![];
627        let active: Vec<ApplicationFile> = Self::active(None).await?;
628        for app in all_files {
629            if app.is_some() {
630                let app = app.unwrap();
631                if active.iter().find(|a| a.id == app.1.id).is_none() && app.1.lifecycle.is_active {
632                    apps.push(app);
633                }
634            }
635        }
636        Ok(apps)
637    }
638
639    pub async fn refill(refill_info: RefillInfo) -> Result<bool, LDNError> {
640        let apps = LDNApplication::merged().await?;
641        if let Some((content, mut app)) = apps.into_iter().find(|(_, app)| app.id == refill_info.id)
642        {
643            let uuid = uuidv4::uuid::v4();
644            let request_id = uuid.clone();
645            let new_request = AllocationRequest::new(
646                "SSA Bot".to_string(),
647                request_id.clone(),
648                AllocationRequestType::Refill(0),
649                format!("{}{}", refill_info.amount, refill_info.amount_type),
650            );
651            let app_file = app.start_refill_request(new_request);
652            Self::issue_refill(app.issue_number.clone()).await?;
653            LDNPullRequest::create_refill_pr(
654                app.id.clone(),
655                app.client.name.clone(),
656                serde_json::to_string_pretty(&app_file).unwrap(),
657                content.path.clone(), // filename
658                request_id.clone(),
659                content.sha,
660            )
661            .await?;
662            return Ok(true);
663        }
664        Err(LDNError::Load("Failed to get application file".to_string()))
665    }
666
667    pub async fn validate_flow(pr_number: u64, actor: &str) -> Result<bool, LDNError> {
668        dbg!(
669            "Validating flow for PR number {} with user handle {}",
670            pr_number,
671            actor
672        );
673
674        let gh = GithubWrapper::new();
675        let author = match gh.get_last_commit_author(pr_number).await {
676            Ok(author) => author,
677            Err(err) => return Err(LDNError::Load(format!("Failed to get last commit author. Reason: {}", err))),
678        };
679
680        if author.is_empty() {
681            return Ok(false);
682        }
683        
684
685        let (_, files) = match gh.get_pull_request_files(pr_number).await {
686            Ok(files) => files,
687            Err(err) => return Err(LDNError::Load(format!("Failed to get pull request files. Reason: {}", err))),
688        };
689
690        if files.len() != 1 {
691            return Ok(false);
692        }
693
694        let branch_name = match gh.get_branch_name_from_pr(pr_number).await {
695            Ok(branch_name) => branch_name,
696            Err(err) => return Err(LDNError::Load(format!("Failed to get pull request. Reason: {}", err))),
697        };
698
699        let application = match gh.get_file(&files[0].filename, &branch_name).await {
700            Ok(file) => LDNApplication::content_items_to_app_file(file)?,
701            Err(err) => return Err(LDNError::Load(format!("Failed to get file content. Reason: {}", err))),
702        };
703
704        //Check if application is in Submitted state
705        if application.lifecycle.get_state() == AppState::Submitted {
706            if !application.lifecycle.validated_by.is_empty() {
707                return Ok(false);
708            }
709            if !application.lifecycle.validated_at.is_empty() {
710                return Ok(false);
711            }
712            let active_request = application.allocation.active();
713            if active_request.is_some() {
714                return Ok(false);
715            }
716            if application.allocation.0.len() > 0 {
717                return Ok(false);
718            }
719            return Ok(true);
720        }
721        
722        //Check if application is in any other state
723        let bot_user = if get_env_var_or_default("FILPLUS_ENV", "dev") == "prod" {
724            PROD_BOT_USER
725        } else {
726            DEV_BOT_USER
727        };  
728
729        if author != bot_user {
730            return Ok(false);
731        }
732
733        return Ok(true);
734        
735    }
736
737    pub async fn validate_trigger(pr_number: u64, actor: &str) -> Result<bool, LDNError> {
738        dbg!(
739            "Validating trigger for PR number {} with user handle {}",
740            pr_number,
741            actor
742        );
743        if let Ok(application_file) = LDNApplication::single_active(pr_number).await {
744            let validated_by = application_file.lifecycle.validated_by.clone();
745            let validated_at = application_file.lifecycle.validated_at.clone();
746            let app_state = application_file.lifecycle.get_state();
747            let valid_rkh = Self::fetch_rkh().await?;
748            let bot_user = if get_env_var_or_default("FILPLUS_ENV", "dev") == "prod" {
749                PROD_BOT_USER
750            } else {
751                DEV_BOT_USER
752            };            
753            let res: bool = match app_state {
754                AppState::Submitted => return Ok(false),
755                AppState::ReadyToSign => {
756                    if application_file.allocation.0.len() > 0
757                        && application_file
758                            .allocation
759                            .0
760                            .get(0)
761                            .unwrap()
762                            .signers
763                            .0
764                            .len()
765                            > 0
766                    {
767                        false
768                    } else if !validated_at.is_empty()
769                        && !validated_by.is_empty()
770                        && actor == bot_user
771                        && valid_rkh.is_valid(&validated_by)
772                    {
773                        true
774                    } else {
775                        false
776                    }
777                }
778                AppState::StartSignDatacap => {
779                    if !validated_at.is_empty()
780                        && !validated_by.is_empty()
781                        && valid_rkh.is_valid(&validated_by)
782                    {
783                        true
784                    } else {
785                        false
786                    }
787                }
788                AppState::Granted => {
789                    if !validated_at.is_empty()
790                        && !validated_by.is_empty()
791                        && valid_rkh.is_valid(&validated_by)
792                    {
793                        true
794                    } else {
795                        false
796                    }
797                }
798                AppState::TotalDatacapReached => true,
799                AppState::Error => return Ok(false),
800            };
801            if res {
802                dbg!("Validated");
803                return Ok(true);
804            }
805            let app_file = application_file.move_back_to_governance_review();
806            let ldn_application = LDNApplication::load(app_file.id.clone()).await?;
807            match LDNPullRequest::add_commit_to(
808                ldn_application.file_name,
809                ldn_application.branch_name.clone(),
810                format!("Move application back to governance review"),
811                serde_json::to_string_pretty(&app_file).unwrap(),
812                ldn_application.file_sha.clone(),
813            )
814            .await
815            {
816                Some(()) => {}
817                None => {}
818            };
819            return Ok(false);
820        };
821        dbg!("Failed to fetch Application File");
822        Ok(false)
823    }
824
825    pub async fn validate_approval(pr_number: u64) -> Result<bool, LDNError> {
826        dbg!("Validating approval for PR number {}", pr_number);
827        match LDNApplication::single_active(pr_number).await {
828            Ok(application_file) => {
829                let app_state: AppState = application_file.lifecycle.get_state();
830                dbg!("Validating approval: App state is {:?}", app_state.as_str());
831                if app_state < AppState::StartSignDatacap {
832                    dbg!("State is less than StartSignDatacap");
833                    return Ok(false);
834                }
835                match app_state {
836                    AppState::StartSignDatacap => {
837                        dbg!("State is StartSignDatacap");
838                        let active_request = application_file.allocation.active();
839                        if active_request.is_none() {
840                            dbg!("No active request");
841                            return Ok(false);
842                        }
843                        let active_request = active_request.unwrap();
844                        let signers: application::file::Notaries = active_request.signers.clone();
845                        if signers.0.len() != 2 {
846                            dbg!("Not enough signers");
847                            return Ok(false);
848                        }
849                        let signer = signers.0.get(1).unwrap();
850                        let signer_address = signer.signing_address.clone();
851                        let valid_notaries = Self::fetch_noatries().await?;
852                        if valid_notaries.is_valid(&signer_address) {
853                            dbg!("Valid notary");
854                            return Ok(true);
855                        }
856                        dbg!("Not a valid notary");
857                        Ok(false)
858                    }
859                    _ => Ok(true),
860                }
861            }
862            Err(e) => Err(LDNError::Load(format!(
863                "PR number {} not found: {}",
864                pr_number, e
865            ))),
866        }
867    }
868
869    pub async fn validate_proposal(pr_number: u64) -> Result<bool, LDNError> {
870        dbg!("Validating proposal for PR number {}", pr_number);
871        match LDNApplication::single_active(pr_number).await {
872            Ok(application_file) => {
873                let app_state: AppState = application_file.lifecycle.get_state();
874                dbg!("Validating proposal: App state is {:?}", app_state.as_str());
875                if app_state < AppState::ReadyToSign {
876                    dbg!("State is less than ReadyToSign");
877                    return Ok(false);
878                }
879                match app_state {
880                    AppState::ReadyToSign => {
881                        let active_request = application_file.allocation.active();
882                        if active_request.is_none() {
883                            dbg!("No active request");
884                            return Ok(false);
885                        }
886                        let active_request = active_request.unwrap();
887                        let signers = active_request.signers.clone();
888                        if signers.0.len() != 1 {
889                            dbg!("Not enough signers");
890                            return Ok(false);
891                        }
892                        let signer = signers.0.get(0).unwrap();
893                        let signer_address = signer.signing_address.clone();
894                        let valid_notaries = Self::fetch_noatries().await?;
895                        if valid_notaries.is_valid(&signer_address) {
896                            dbg!("Valid notary");
897                            return Ok(true);
898                        }
899                        dbg!("Not a valid notary");
900                        Ok(false)
901                    }
902                    _ => Ok(true),
903                }
904            }
905            Err(e) => Err(LDNError::Load(format!(
906                "PR number {} not found: {}",
907                pr_number, e
908            ))),
909        }
910    }
911
912    async fn issue_waiting_for_gov_review(issue_number: String) -> Result<bool, LDNError> {
913        let gh = GithubWrapper::new();
914        gh.add_comment_to_issue(
915            issue_number.parse().unwrap(),
916            "Application is waiting for governance review",
917        )
918        .await
919        .map_err(|e| {
920            return LDNError::New(format!(
921                "Error adding comment to issue {} /// {}",
922                issue_number, e
923            ));
924        })?;
925        gh.replace_issue_labels(
926            issue_number.parse().unwrap(),
927            &["waiting for governance review".to_string()],
928        )
929        .await
930        .map_err(|e| {
931            return LDNError::New(format!(
932                "Error add label to issue {} /// {}",
933                issue_number, e
934            ));
935        })?;
936
937        Ok(true)
938    }
939    async fn issue_ready_to_sign(issue_number: String) -> Result<bool, LDNError> {
940        let gh = GithubWrapper::new();
941        gh.add_comment_to_issue(
942            issue_number.parse().unwrap(),
943            "Application is ready to sign",
944        )
945        .await
946        .map_err(|e| {
947            return LDNError::New(format!(
948                "Error adding comment to issue {} /// {}",
949                issue_number, e
950            ));
951        })
952        .unwrap();
953        gh.replace_issue_labels(
954            issue_number.parse().unwrap(),
955            &["ready to sign".to_string()],
956        )
957        .await
958        .map_err(|e| {
959            return LDNError::New(format!(
960                "Error adding comment to issue {} /// {}",
961                issue_number, e
962            ));
963        })
964        .unwrap();
965        Ok(true)
966    }
967    async fn issue_start_sign_dc(issue_number: String) -> Result<bool, LDNError> {
968        let gh = GithubWrapper::new();
969        gh.add_comment_to_issue(
970            issue_number.parse().unwrap(),
971            "Application is in the process of signing datacap",
972        )
973        .await
974        .map_err(|e| {
975            return LDNError::New(format!(
976                "Error adding comment to issue {} /// {}",
977                issue_number, e
978            ));
979        })
980        .unwrap();
981        gh.replace_issue_labels(
982            issue_number.parse().unwrap(),
983            &["Start Sign Datacap".to_string()],
984        )
985        .await
986        .map_err(|e| {
987            return LDNError::New(format!(
988                "Error adding comment to issue {} /// {}",
989                issue_number, e
990            ));
991        })
992        .unwrap();
993        Ok(true)
994    }
995    async fn issue_granted(issue_number: String) -> Result<bool, LDNError> {
996        let gh = GithubWrapper::new();
997        gh.add_comment_to_issue(issue_number.parse().unwrap(), "Application is Granted")
998            .await
999            .map_err(|e| {
1000                return LDNError::New(format!(
1001                    "Error adding comment to issue {} /// {}",
1002                    issue_number, e
1003                ));
1004            })
1005            .unwrap();
1006        gh.replace_issue_labels(issue_number.parse().unwrap(), &["Granted".to_string()])
1007            .await
1008            .map_err(|e| {
1009                return LDNError::New(format!(
1010                    "Error adding comment to issue {} /// {}",
1011                    issue_number, e
1012                ));
1013            })
1014            .unwrap();
1015        Ok(true)
1016    }
1017    async fn issue_refill(issue_number: String) -> Result<bool, LDNError> {
1018        let gh = GithubWrapper::new();
1019        gh.add_comment_to_issue(issue_number.parse().unwrap(), "Application is in Refill")
1020            .await
1021            .map_err(|e| {
1022                return LDNError::New(format!(
1023                    "Error adding comment to issue {} /// {}",
1024                    issue_number, e
1025                ));
1026            })
1027            .unwrap();
1028        gh.replace_issue_labels(issue_number.parse().unwrap(), &["Refill".to_string()])
1029            .await
1030            .map_err(|e| {
1031                return LDNError::New(format!(
1032                    "Error adding comment to issue {} /// {}",
1033                    issue_number, e
1034                ));
1035            })
1036            .unwrap();
1037        Ok(true)
1038    }
1039    async fn issue_full_dc(issue_number: String) -> Result<bool, LDNError> {
1040        let gh = GithubWrapper::new();
1041        gh.add_comment_to_issue(issue_number.parse().unwrap(), "Application is Completed")
1042            .await
1043            .map_err(|e| {
1044                return LDNError::New(format!(
1045                    "Error adding comment to issue {} /// {}",
1046                    issue_number, e
1047                ));
1048            })
1049            .unwrap();
1050        gh.replace_issue_labels(
1051            issue_number.parse().unwrap(),
1052            &["Completed".to_string(), "Reached Total Datacap".to_string()],
1053        )
1054        .await
1055        .map_err(|e| {
1056            return LDNError::New(format!(
1057                "Error adding comment to issue {} /// {}",
1058                issue_number, e
1059            ));
1060        })
1061        .unwrap();
1062        Ok(true)
1063    }
1064}
1065
1066#[derive(Serialize, Deserialize, Debug)]
1067pub struct LDNPullRequest {
1068    pub branch_name: String,
1069    pub title: String,
1070    pub body: String,
1071    pub path: String,
1072}
1073
1074impl LDNPullRequest {
1075    async fn create_pr(
1076        application_id: String,
1077        owner_name: String,
1078        app_branch_name: String,
1079        file_name: String,
1080        file_content: String,
1081    ) -> Result<String, LDNError> {
1082        let initial_commit = Self::application_initial_commit(&owner_name, &application_id);
1083        let gh: GithubWrapper = GithubWrapper::new();
1084        let head_hash = gh.get_main_branch_sha().await.unwrap();
1085        let create_ref_request = gh
1086            .build_create_ref_request(app_branch_name.clone(), head_hash)
1087            .map_err(|e| {
1088                return LDNError::New(format!(
1089                    "Application issue {} cannot create branch /// {}",
1090                    application_id, e
1091                ));
1092            })?;
1093
1094        let (_pr, file_sha) = gh
1095            .create_merge_request(CreateMergeRequestData {
1096                application_id: application_id.clone(),
1097                branch_name: app_branch_name,
1098                file_name,
1099                owner_name,
1100                ref_request: create_ref_request,
1101                file_content,
1102                commit: initial_commit,
1103            })
1104            .await
1105            .map_err(|e| {
1106                return LDNError::New(format!(
1107                    "Application issue {} cannot create merge request /// {}",
1108                    application_id, e
1109                ));
1110            })?;
1111
1112        Ok(file_sha)
1113    }
1114
1115    async fn create_refill_pr(
1116        application_id: String,
1117        owner_name: String,
1118        file_content: String,
1119        file_name: String,
1120        branch_name: String,
1121        file_sha: String,
1122    ) -> Result<u64, LDNError> {
1123        let initial_commit = Self::application_initial_commit(&owner_name, &application_id);
1124        let gh: GithubWrapper = GithubWrapper::new();
1125        let head_hash = gh.get_main_branch_sha().await.unwrap();
1126        let create_ref_request = gh
1127            .build_create_ref_request(branch_name.clone(), head_hash)
1128            .map_err(|e| {
1129                return LDNError::New(format!(
1130                    "Application issue {} cannot create branch /// {}",
1131                    application_id, e
1132                ));
1133            })?;
1134        let pr = match gh
1135            .create_refill_merge_request(CreateRefillMergeRequestData {
1136                application_id: application_id.clone(),
1137                owner_name,
1138                file_name,
1139                file_sha,
1140                ref_request: create_ref_request,
1141                branch_name,
1142                file_content,
1143                commit: initial_commit,
1144            })
1145            .await
1146        {
1147            Ok(pr) => pr,
1148            Err(e) => {
1149                return Err(LDNError::New(format!(
1150                    "Application issue {} cannot create branch /// {}",
1151                    application_id, e
1152                )));
1153            }
1154        };
1155        Ok(pr.0.number)
1156    }
1157
1158    pub(super) async fn add_commit_to(
1159        path: String,
1160        branch_name: String,
1161        commit_message: String,
1162        new_content: String,
1163        file_sha: String,
1164    ) -> Option<()> {
1165        let gh: GithubWrapper = GithubWrapper::new();
1166        match gh
1167            .update_file_content(
1168                &path,
1169                &commit_message,
1170                &new_content,
1171                &branch_name,
1172                &file_sha,
1173            )
1174            .await
1175        {
1176            Ok(_) => Some(()),
1177            Err(_) => None,
1178        }
1179    }
1180
1181    pub(super) fn application_branch_name(application_id: &str) -> String {
1182        format!("Application/{}", application_id)
1183    }
1184
1185    pub(super) fn application_path(application_id: &str) -> String {
1186        format!("{}/{}.json", "applications", application_id)
1187    }
1188
1189    pub(super) fn application_initial_commit(owner_name: &str, application_id: &str) -> String {
1190        format!("Start Application: {}-{}", owner_name, application_id)
1191    }
1192
1193    pub(super) fn application_move_to_proposal_commit(actor: &str) -> String {
1194        format!(
1195            "Governance Team User {} Moved Application to Proposal State from Governance Review State",
1196            actor
1197        )
1198    }
1199
1200    pub(super) fn application_move_to_approval_commit(actor: &str) -> String {
1201        format!(
1202            "Notary User {} Moved Application to Approval State from Proposal State",
1203            actor
1204        )
1205    }
1206
1207    pub(super) fn application_move_to_confirmed_commit(actor: &str) -> String {
1208        format!(
1209            "Notary User {} Moved Application to Confirmed State from Proposal Approval",
1210            actor
1211        )
1212    }
1213}
1214
1215pub fn get_file_sha(content: &ContentItems) -> Option<String> {
1216    match content.items.get(0) {
1217        Some(item) => {
1218            let sha = item.sha.clone();
1219            Some(sha)
1220        }
1221        None => None,
1222    }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227    use super::*;
1228    use env_logger::{Builder, Env};
1229    use tokio::time::{sleep, Duration};
1230
1231    #[tokio::test]
1232    async fn end_to_end() {
1233        // Set logging
1234        Builder::from_env(Env::default().default_filter_or("info")).init();
1235        log::info!("Starting end-to-end test");
1236
1237        // Test Creating an application
1238        let gh: GithubWrapper = GithubWrapper::new();
1239
1240        log::info!("Creating a new LDNApplication from issue");
1241        let ldn_application = match LDNApplication::new_from_issue(CreateApplicationInfo {
1242            issue_number: "471".to_string(),
1243        })
1244        .await
1245        {
1246            Ok(app) => app,
1247            Err(e) => {
1248                log::error!("Failed to create LDNApplication: {}", e);
1249                return;
1250            }
1251        };
1252
1253        let application_id = ldn_application.application_id.to_string();
1254        log::info!("LDNApplication created with ID: {}", application_id);
1255
1256        // Validate file creation
1257        log::info!("Validating file creation for application");
1258        if let Err(e) = gh
1259            .get_file(&ldn_application.file_name, &ldn_application.branch_name)
1260            .await
1261        {
1262            log::warn!(
1263                "File validation failed for application ID {}: {}",
1264                application_id,
1265                e
1266            );
1267        }
1268
1269        // Validate pull request creation
1270        log::info!("Validating pull request creation for application");
1271        if let Err(e) = gh
1272            .get_pull_request_by_head(&LDNPullRequest::application_branch_name(
1273                application_id.as_str(),
1274            ))
1275            .await
1276        {
1277            log::warn!(
1278                "Pull request validation failed for application ID {}: {}",
1279                application_id,
1280                e
1281            );
1282        }
1283
1284        sleep(Duration::from_millis(2000)).await;
1285
1286        // Test Triggering an application
1287        log::info!("Loading application for triggering");
1288        let ldn_application_before_trigger =
1289            match LDNApplication::load(application_id.clone()).await {
1290                Ok(app) => app,
1291                Err(e) => {
1292                    log::error!("Failed to load application for triggering: {}", e);
1293                    return;
1294                }
1295            };
1296
1297        log::info!("Completing governance review");
1298        if let Err(e) = ldn_application_before_trigger
1299            .complete_governance_review(CompleteGovernanceReviewInfo {
1300                actor: "actor_address".to_string(),
1301            })
1302            .await
1303        {
1304            log::error!("Failed to complete governance review: {}", e);
1305            return;
1306        }
1307
1308        let ldn_application_after_trigger = match LDNApplication::load(application_id.clone()).await
1309        {
1310            Ok(app) => app,
1311            Err(e) => {
1312                log::error!("Failed to load application after triggering: {}", e);
1313                return;
1314            }
1315        };
1316
1317        assert_eq!(
1318            ldn_application_after_trigger.app_state().await.unwrap(),
1319            AppState::ReadyToSign
1320        );
1321        log::info!("Application state updated to ReadyToSign");
1322        sleep(Duration::from_millis(2000)).await;
1323
1324        // Cleanup
1325        log::info!("Starting cleanup process");
1326        let head = &LDNPullRequest::application_branch_name(&application_id);
1327        match gh.get_pull_request_by_head(head).await {
1328            Ok(prs) => {
1329                if let Some(pr) = prs.get(0) {
1330                    let number = pr.number;
1331                    match gh.merge_pull_request(number).await {
1332                        Ok(_) => log::info!("Merged pull request {}", number),
1333                        Err(_) => log::info!("Pull request {} was already merged", number),
1334                    };
1335                }
1336            }
1337            Err(e) => log::warn!("Failed to get pull request by head: {}", e),
1338        };
1339
1340        sleep(Duration::from_millis(3000)).await;
1341
1342        let file = match gh.get_file(&ldn_application.file_name, "main").await {
1343            Ok(f) => f,
1344            Err(e) => {
1345                log::error!("Failed to get file: {}", e);
1346                return;
1347            }
1348        };
1349
1350        let file_sha = file.items[0].sha.clone();
1351        let remove_file_request = gh
1352            .delete_file(&ldn_application.file_name, "main", "remove file", &file_sha)
1353            .await;
1354        let remove_branch_request = gh
1355            .build_remove_ref_request(LDNPullRequest::application_branch_name(&application_id))
1356            .unwrap();
1357
1358        if let Err(e) = gh.remove_branch(remove_branch_request).await {
1359            log::warn!("Failed to remove branch: {}", e);
1360        }
1361        if let Err(e) = remove_file_request {
1362            log::warn!("Failed to remove file: {}", e);
1363        }
1364
1365        log::info!(
1366            "End-to-end test completed for application ID: {}",
1367            application_id
1368        );
1369    }
1370}