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 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 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 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 async fn app_state(&self) -> Result<AppState, LDNError> {
451 let f = self.file().await?;
452 Ok(f.lifecycle.get_state())
453 }
454
455 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 = ¬aries.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(), 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 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 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 Builder::from_env(Env::default().default_filter_or("info")).init();
1235 log::info!("Starting end-to-end test");
1236
1237 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 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 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 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 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}