1use crate::artifact::Artifact;
28use crate::contract::Network;
29use crate::errors::ArtifactError;
30use crate::{Address, Contract, DeploymentInformation, TransactionHash};
31use serde::Deserialize;
32use serde_json::{from_reader, from_slice, from_str, from_value, Value};
33use std::collections::HashMap;
34use std::fs::File;
35use std::io::{BufReader, Read};
36use std::path::Path;
37
38#[must_use = "hardhat loaders do nothing unless you load them"]
56pub struct HardHatLoader {
57 pub origin: Option<String>,
60
61 pub networks_allow_list: Vec<NetworkEntry>,
70
71 pub networks_deny_list: Vec<NetworkEntry>,
81
82 pub contracts_allow_list: Vec<String>,
89
90 pub contracts_deny_list: Vec<String>,
100}
101
102impl HardHatLoader {
103 pub fn new() -> Self {
105 HardHatLoader {
106 origin: None,
107 networks_deny_list: Vec::new(),
108 networks_allow_list: Vec::new(),
109 contracts_allow_list: Vec::new(),
110 contracts_deny_list: Vec::new(),
111 }
112 }
113
114 pub fn with_origin(origin: impl Into<String>) -> Self {
116 HardHatLoader {
117 origin: Some(origin.into()),
118 networks_deny_list: Vec::new(),
119 networks_allow_list: Vec::new(),
120 contracts_allow_list: Vec::new(),
121 contracts_deny_list: Vec::new(),
122 }
123 }
124
125 pub fn origin(mut self, origin: impl Into<String>) -> Self {
129 self.origin = Some(origin.into());
130 self
131 }
132
133 pub fn allow_network_by_chain_id(mut self, network: impl Into<String>) -> Self {
137 self.networks_allow_list
138 .push(NetworkEntry::ByChainId(network.into()));
139 self
140 }
141
142 pub fn allow_network_by_name(mut self, network: impl Into<String>) -> Self {
146 self.networks_allow_list
147 .push(NetworkEntry::ByName(network.into()));
148 self
149 }
150
151 pub fn deny_network_by_chain_id(mut self, network: impl Into<String>) -> Self {
155 self.networks_deny_list
156 .push(NetworkEntry::ByChainId(network.into()));
157 self
158 }
159
160 pub fn deny_network_by_name(mut self, network: impl Into<String>) -> Self {
164 self.networks_deny_list
165 .push(NetworkEntry::ByName(network.into()));
166 self
167 }
168
169 pub fn allow_contract(mut self, contract: impl Into<String>) -> Self {
173 self.contracts_allow_list.push(contract.into());
174 self
175 }
176
177 pub fn deny_contract(mut self, contract: impl Into<String>) -> Self {
181 self.contracts_deny_list.push(contract.into());
182 self
183 }
184
185 pub fn load_from_reader(&self, f: Format, v: impl Read) -> Result<Artifact, ArtifactError> {
187 self.load_artifact(f, "<unknown>", v, from_reader, from_reader)
188 }
189
190 pub fn load_from_slice(&self, f: Format, v: &[u8]) -> Result<Artifact, ArtifactError> {
192 self.load_artifact(f, "<unknown>", v, from_slice, from_slice)
193 }
194
195 pub fn load_from_str(&self, f: Format, v: &str) -> Result<Artifact, ArtifactError> {
197 self.load_artifact(f, "<unknown>", v, from_str, from_str)
198 }
199
200 pub fn load_from_value(&self, f: Format, v: Value) -> Result<Artifact, ArtifactError> {
202 self.load_artifact(f, "<unknown>", v, from_value, from_value)
203 }
204
205 pub fn load_from_file(
207 &self,
208 f: Format,
209 p: impl AsRef<Path>,
210 ) -> Result<Artifact, ArtifactError> {
211 let path = p.as_ref();
212 let file = File::open(path)?;
213 let reader = BufReader::new(file);
214 self.load_artifact(f, path.display(), reader, from_reader, from_reader)
215 }
216
217 pub fn load_from_directory(&self, p: impl AsRef<Path>) -> Result<Artifact, ArtifactError> {
219 self._load_from_directory(p.as_ref())
220 }
221
222 fn _load_from_directory(&self, p: &Path) -> Result<Artifact, ArtifactError> {
260 let mut artifact = Artifact::with_origin(p.display().to_string());
261
262 let mut chain_id_buf = String::new();
263
264 for chain_entry in p.read_dir()? {
265 let chain_entry = chain_entry?;
266
267 let chain_path = chain_entry.path();
268 if !chain_path.is_dir() {
269 continue;
270 }
271
272 let chain_id_file = chain_path.join(".chainId");
273 if !chain_id_file.exists() {
274 continue;
275 }
276
277 chain_id_buf.clear();
278 File::open(chain_id_file)?.read_to_string(&mut chain_id_buf)?;
279 let chain_id = chain_id_buf.trim().to_string();
280
281 let chain_name = chain_path
282 .file_name()
283 .ok_or_else(|| {
284 std::io::Error::new(
285 std::io::ErrorKind::Other,
286 format!("unable to get directory name for path {:?}", chain_path),
287 )
288 })?
289 .to_string_lossy();
290
291 if !self.network_allowed(&chain_id, &chain_name) {
292 continue;
293 }
294
295 for contract_entry in chain_path.read_dir()? {
296 let contract_entry = contract_entry?;
297
298 let contract_path = contract_entry.path();
299 if !contract_path.is_file() {
300 continue;
301 }
302
303 let mut contract_name = contract_path
304 .file_name()
305 .ok_or_else(|| {
306 std::io::Error::new(
307 std::io::ErrorKind::Other,
308 format!("unable to get file name for path {:?}", contract_path),
309 )
310 })?
311 .to_string_lossy()
312 .into_owned();
313
314 if !contract_name.ends_with(".json") {
315 continue;
316 }
317
318 contract_name.truncate(contract_name.len() - ".json".len());
319
320 if !self.contract_allowed(&contract_name) {
321 continue;
322 }
323
324 let HardHatContract {
325 address,
326 transaction_hash,
327 mut contract,
328 } = {
329 let file = File::open(contract_path)?;
330 let reader = BufReader::new(file);
331 from_reader(reader)?
332 };
333
334 contract.name = contract_name;
335
336 self.add_contract_to_artifact(
337 &mut artifact,
338 contract,
339 chain_id.clone(),
340 address,
341 transaction_hash,
342 )?;
343 }
344 }
345
346 Ok(artifact)
347 }
348
349 fn load_artifact<T>(
350 &self,
351 format: Format,
352 origin: impl ToString,
353 source: T,
354 single_loader: impl FnOnce(T) -> serde_json::Result<HardHatExport>,
355 multi_loader: impl FnOnce(T) -> serde_json::Result<HardHatMultiExport>,
356 ) -> Result<Artifact, ArtifactError> {
357 let origin = self.origin.clone().unwrap_or_else(|| origin.to_string());
358
359 let mut artifact = Artifact::with_origin(origin);
360
361 match format {
362 Format::SingleExport => {
363 let loaded = single_loader(source)?;
364 self.fill_artifact(&mut artifact, loaded)?
365 }
366 Format::MultiExport => {
367 let loaded = multi_loader(source)?;
368 self.fill_artifact_multi(&mut artifact, loaded)?
369 }
370 }
371
372 Ok(artifact)
373 }
374
375 fn fill_artifact(
376 &self,
377 artifact: &mut Artifact,
378 export: HardHatExport,
379 ) -> Result<(), ArtifactError> {
380 if self.network_allowed(&export.chain_id, &export.chain_name) {
381 for (name, contract) in export.contracts {
382 let HardHatContract {
383 address,
384 transaction_hash,
385 mut contract,
386 } = contract;
387
388 if !self.contract_allowed(&name) {
389 continue;
390 }
391
392 contract.name = name;
393
394 self.add_contract_to_artifact(
395 artifact,
396 contract,
397 export.chain_id.clone(),
398 address,
399 transaction_hash,
400 )?;
401 }
402 }
403
404 Ok(())
405 }
406
407 fn fill_artifact_multi(
408 &self,
409 artifact: &mut Artifact,
410 export: HardHatMultiExport,
411 ) -> Result<(), ArtifactError> {
412 for (_, export) in export.networks {
413 for (_, export) in export {
414 self.fill_artifact(artifact, export)?;
415 }
416 }
417
418 Ok(())
419 }
420
421 fn add_contract_to_artifact(
422 &self,
423 artifact: &mut Artifact,
424 contract: Contract,
425 chain_id: String,
426 address: Address,
427 transaction_hash: Option<TransactionHash>,
428 ) -> Result<(), ArtifactError> {
429 let contract_guard = artifact.get_mut(&contract.name);
430 let mut contract = if let Some(existing_contract) = contract_guard {
431 if existing_contract.interface != contract.interface {
432 return Err(ArtifactError::AbiMismatch(contract.name));
433 }
434
435 existing_contract
436 } else {
437 drop(contract_guard);
440 artifact.insert(contract).inserted_contract
441 };
442
443 let deployment_information = transaction_hash.map(DeploymentInformation::TransactionHash);
444
445 if contract.networks.contains_key(&chain_id) {
446 Err(ArtifactError::DuplicateChain(chain_id))
447 } else {
448 contract.networks_mut().insert(
449 chain_id,
450 Network {
451 address,
452 deployment_information,
453 },
454 );
455
456 Ok(())
457 }
458 }
459
460 fn contract_allowed(&self, name: &str) -> bool {
461 !self.contract_explicitly_denied(name)
462 && (self.contracts_allow_list.is_empty() || self.contract_explicitly_allowed(name))
463 }
464
465 fn contract_explicitly_allowed(&self, name: &str) -> bool {
466 self.contracts_allow_list.iter().any(|x| x == name)
467 }
468
469 fn contract_explicitly_denied(&self, name: &str) -> bool {
470 self.contracts_deny_list.iter().any(|x| x == name)
471 }
472
473 fn network_allowed(&self, chain_id: &str, chain_name: &str) -> bool {
474 !self.network_explicitly_denied(chain_id, chain_name)
475 && (self.networks_allow_list.is_empty()
476 || self.network_explicitly_allowed(chain_id, chain_name))
477 }
478
479 fn network_explicitly_allowed(&self, chain_id: &str, chain_name: &str) -> bool {
480 self.networks_allow_list
481 .iter()
482 .any(|x| x.matches(chain_id, chain_name))
483 }
484
485 fn network_explicitly_denied(&self, chain_id: &str, chain_name: &str) -> bool {
486 self.networks_deny_list
487 .iter()
488 .any(|x| x.matches(chain_id, chain_name))
489 }
490}
491
492impl Default for HardHatLoader {
493 fn default() -> Self {
494 HardHatLoader::new()
495 }
496}
497
498#[derive(Copy, Clone, Debug, Eq, PartialEq)]
500pub enum Format {
501 SingleExport,
503
504 MultiExport,
506}
507
508#[derive(Clone, Debug)]
510pub enum NetworkEntry {
511 ByChainId(String),
513
514 ByName(String),
516}
517
518impl NetworkEntry {
519 fn matches(&self, chain_id: &str, chain_name: &str) -> bool {
520 match self {
521 NetworkEntry::ByChainId(id) => chain_id == id,
522 NetworkEntry::ByName(name) => chain_name == name,
523 }
524 }
525}
526
527#[derive(Deserialize)]
528struct HardHatMultiExport {
529 #[serde(flatten)]
530 networks: HashMap<String, HashMap<String, HardHatExport>>,
531}
532
533#[derive(Deserialize)]
534struct HardHatExport {
535 #[serde(rename = "name")]
536 chain_name: String,
537 #[serde(rename = "chainId")]
538 chain_id: String,
539 contracts: HashMap<String, HardHatContract>,
540}
541
542#[derive(Deserialize)]
543struct HardHatContract {
544 address: Address,
545 #[serde(rename = "transactionHash")]
546 transaction_hash: Option<TransactionHash>,
547 #[serde(flatten)]
548 contract: Contract,
549}
550
551#[cfg(test)]
552mod test {
553 use super::*;
554 use std::path::PathBuf;
555 use web3::ethabi::ethereum_types::BigEndianHash;
556 use web3::types::{H256, U256};
557
558 fn address(address: u8) -> Address {
559 Address::from(H256::from_uint(&U256::from(address)))
560 }
561
562 #[test]
563 fn load_single() {
564 let json = r#"
565 {
566 "name": "mainnet",
567 "chainId": "1",
568 "contracts": {
569 "A": {
570 "address": "0x000000000000000000000000000000000000000A"
571 },
572 "B": {
573 "address": "0x000000000000000000000000000000000000000B"
574 }
575 }
576 }
577 "#;
578
579 let artifact = HardHatLoader::new()
580 .load_from_str(Format::SingleExport, json)
581 .unwrap();
582
583 assert_eq!(artifact.len(), 2);
584
585 let a = artifact.get("A").unwrap();
586 assert_eq!(a.name, "A");
587 assert_eq!(a.networks.len(), 1);
588 assert_eq!(a.networks["1"].address, address(0xA));
589
590 let b = artifact.get("B").unwrap();
591 assert_eq!(b.name, "B");
592 assert_eq!(b.networks.len(), 1);
593 assert_eq!(b.networks["1"].address, address(0xB));
594 }
595
596 static MULTI_EXPORT: &str = r#"
597 {
598 "1": {
599 "mainnet": {
600 "name": "mainnet",
601 "chainId": "1",
602 "contracts": {
603 "A": {
604 "address": "0x000000000000000000000000000000000000000A"
605 },
606 "B": {
607 "address": "0x000000000000000000000000000000000000000B"
608 }
609 }
610 }
611 },
612 "4": {
613 "rinkeby": {
614 "name": "rinkeby",
615 "chainId": "4",
616 "contracts": {
617 "A": {
618 "address": "0x00000000000000000000000000000000000000AA"
619 }
620 }
621 }
622 }
623 }
624 "#;
625
626 #[test]
627 fn load_multi() {
628 let artifact = HardHatLoader::new()
629 .load_from_str(Format::MultiExport, MULTI_EXPORT)
630 .unwrap();
631
632 assert_eq!(artifact.len(), 2);
633
634 let a = artifact.get("A").unwrap();
635 assert_eq!(a.name, "A");
636 assert_eq!(a.networks.len(), 2);
637 assert_eq!(a.networks["1"].address, address(0xA));
638 assert_eq!(a.networks["4"].address, address(0xAA));
639
640 let b = artifact.get("B").unwrap();
641 assert_eq!(b.name, "B");
642 assert_eq!(b.networks.len(), 1);
643 assert_eq!(b.networks["1"].address, address(0xB));
644 }
645
646 #[test]
647 fn load_multi_duplicate_networks_ok() {
648 let json = r#"
649 {
650 "1": {
651 "mainnet": {
652 "name": "mainnet",
653 "chainId": "1",
654 "contracts": {
655 "A": {
656 "address": "0x000000000000000000000000000000000000000A"
657 }
658 }
659 },
660 "mainnet_beta": {
661 "name": "mainnet_beta",
662 "chainId": "1",
663 "contracts": {
664 "B": {
665 "address": "0x000000000000000000000000000000000000000B"
666 }
667 }
668 }
669 }
670 }
671 "#;
672
673 let artifact = HardHatLoader::new()
674 .load_from_str(Format::MultiExport, json)
675 .unwrap();
676
677 assert_eq!(artifact.len(), 2);
678
679 let a = artifact.get("A").unwrap();
680 assert_eq!(a.name, "A");
681 assert_eq!(a.networks.len(), 1);
682 assert_eq!(a.networks["1"].address, address(0xA));
683
684 let b = artifact.get("B").unwrap();
685 assert_eq!(b.name, "B");
686 assert_eq!(b.networks.len(), 1);
687 assert_eq!(b.networks["1"].address, address(0xB));
688 }
689
690 #[test]
691 fn load_multi_duplicate_networks_err() {
692 let json = r#"
693 {
694 "1": {
695 "mainnet": {
696 "name": "mainnet",
697 "chainId": "1",
698 "contracts": {
699 "A": {
700 "address": "0x000000000000000000000000000000000000000A"
701 }
702 }
703 },
704 "mainnet_beta": {
705 "name": "mainnet_beta",
706 "chainId": "1",
707 "contracts": {
708 "A": {
709 "address": "0x00000000000000000000000000000000000000AA"
710 }
711 }
712 }
713 }
714 }
715 "#;
716
717 let err = HardHatLoader::new().load_from_str(Format::MultiExport, json);
718
719 match err {
720 Err(ArtifactError::DuplicateChain(chain_id)) => assert_eq!(chain_id, "1"),
721 Err(unexpected_err) => panic!("unexpected error {:?}", unexpected_err),
722 _ => panic!("didn't throw an error"),
723 }
724 }
725
726 #[test]
727 fn load_multi_mismatching_abi() {
728 let json = r#"
729 {
730 "1": {
731 "mainnet": {
732 "name": "mainnet",
733 "chainId": "1",
734 "contracts": {
735 "A": {
736 "address": "0x000000000000000000000000000000000000000A",
737 "abi": [
738 {
739 "constant": false,
740 "inputs": [],
741 "name": "foo",
742 "outputs": [],
743 "payable": false,
744 "stateMutability": "nonpayable",
745 "type": "function"
746 }
747 ]
748 }
749 }
750 }
751 },
752 "4": {
753 "rinkeby": {
754 "name": "rinkeby",
755 "chainId": "4",
756 "contracts": {
757 "A": {
758 "address": "0x00000000000000000000000000000000000000AA",
759 "abi": [
760 {
761 "constant": false,
762 "inputs": [],
763 "name": "bar",
764 "outputs": [],
765 "payable": false,
766 "stateMutability": "nonpayable",
767 "type": "function"
768 }
769 ]
770 }
771 }
772 }
773 }
774 }
775 "#;
776
777 let err = HardHatLoader::new().load_from_str(Format::MultiExport, json);
778
779 match err {
780 Err(ArtifactError::AbiMismatch(name)) => assert_eq!(name, "A"),
781 Err(unexpected_err) => panic!("unexpected error {:?}", unexpected_err),
782 _ => panic!("didn't throw an error"),
783 }
784 }
785
786 static NETWORK_CONFLICTS: &str = r#"
787 {
788 "1": {
789 "mainnet": {
790 "name": "mainnet",
791 "chainId": "1",
792 "contracts": {
793 "A": {
794 "address": "0x000000000000000000000000000000000000000A"
795 }
796 }
797 },
798 "mainnet_beta": {
799 "name": "mainnet_beta",
800 "chainId": "1",
801 "contracts": {
802 "A": {
803 "address": "0x00000000000000000000000000000000000000AA",
804 "abi": [
805 {
806 "constant": false,
807 "inputs": [],
808 "name": "test_method",
809 "outputs": [],
810 "payable": false,
811 "stateMutability": "nonpayable",
812 "type": "function"
813 }
814 ]
815 }
816 }
817 }
818 },
819 "4": {
820 "rinkeby": {
821 "name": "rinkeby",
822 "chainId": "4",
823 "contracts": {
824 "A": {
825 "address": "0x00000000000000000000000000000000000000BA"
826 }
827 }
828 }
829 }
830 }
831 "#;
832
833 #[test]
834 fn load_multi_allow_by_name() {
835 let artifact = HardHatLoader::new()
836 .allow_network_by_name("mainnet")
837 .allow_network_by_name("rinkeby")
838 .load_from_str(Format::MultiExport, NETWORK_CONFLICTS)
839 .unwrap();
840
841 assert_eq!(artifact.len(), 1);
842
843 let a = artifact.get("A").unwrap();
844 assert_eq!(a.name, "A");
845 assert_eq!(a.networks.len(), 2);
846 assert_eq!(a.networks["1"].address, address(0xA));
847 assert_eq!(a.networks["4"].address, address(0xBA));
848 }
849
850 #[test]
851 fn load_multi_allow_by_chain_id() {
852 let artifact = HardHatLoader::new()
853 .allow_network_by_chain_id("4")
854 .load_from_str(Format::MultiExport, NETWORK_CONFLICTS)
855 .unwrap();
856
857 assert_eq!(artifact.len(), 1);
858
859 let a = artifact.get("A").unwrap();
860 assert_eq!(a.name, "A");
861 assert_eq!(a.networks.len(), 1);
862 assert_eq!(a.networks["4"].address, address(0xBA));
863 }
864
865 #[test]
866 fn load_multi_deny_by_name() {
867 let artifact = HardHatLoader::new()
868 .deny_network_by_name("mainnet_beta")
869 .load_from_str(Format::MultiExport, NETWORK_CONFLICTS)
870 .unwrap();
871
872 assert_eq!(artifact.len(), 1);
873
874 let a = artifact.get("A").unwrap();
875 assert_eq!(a.name, "A");
876 assert_eq!(a.networks.len(), 2);
877 assert_eq!(a.networks["1"].address, address(0xA));
878 assert_eq!(a.networks["4"].address, address(0xBA));
879 }
880
881 #[test]
882 fn load_multi_deny_by_chain_id() {
883 let artifact = HardHatLoader::new()
884 .deny_network_by_chain_id("1")
885 .load_from_str(Format::MultiExport, NETWORK_CONFLICTS)
886 .unwrap();
887
888 assert_eq!(artifact.len(), 1);
889
890 let a = artifact.get("A").unwrap();
891 assert_eq!(a.name, "A");
892 assert_eq!(a.networks.len(), 1);
893 assert_eq!(a.networks["4"].address, address(0xBA));
894 }
895
896 #[test]
897 fn load_multi_allow_contract_name() {
898 let artifact = HardHatLoader::new()
899 .allow_contract("A")
900 .load_from_str(Format::MultiExport, MULTI_EXPORT)
901 .unwrap();
902
903 assert_eq!(artifact.len(), 1);
904
905 let a = artifact.get("A").unwrap();
906 assert_eq!(a.name, "A");
907 assert_eq!(a.networks.len(), 2);
908 assert_eq!(a.networks["1"].address, address(0xA));
909 assert_eq!(a.networks["4"].address, address(0xAA));
910
911 let artifact = HardHatLoader::new()
912 .allow_contract("X")
913 .load_from_str(Format::MultiExport, MULTI_EXPORT)
914 .unwrap();
915
916 assert_eq!(artifact.len(), 0);
917 }
918
919 #[test]
920 fn load_multi_deny_contract_name() {
921 let artifact = HardHatLoader::new()
922 .deny_contract("A")
923 .load_from_str(Format::MultiExport, MULTI_EXPORT)
924 .unwrap();
925
926 assert_eq!(artifact.len(), 1);
927
928 let a = artifact.get("B").unwrap();
929 assert_eq!(a.name, "B");
930 assert_eq!(a.networks.len(), 1);
931 assert_eq!(a.networks["1"].address, address(0xB));
932
933 let artifact = HardHatLoader::new()
934 .deny_contract("X")
935 .load_from_str(Format::MultiExport, MULTI_EXPORT)
936 .unwrap();
937
938 assert_eq!(artifact.len(), 2);
939
940 let a = artifact.get("A").unwrap();
941 assert_eq!(a.name, "A");
942 assert_eq!(a.networks.len(), 2);
943 assert_eq!(a.networks["1"].address, address(0xA));
944 assert_eq!(a.networks["4"].address, address(0xAA));
945
946 let b = artifact.get("B").unwrap();
947 assert_eq!(b.name, "B");
948 assert_eq!(b.networks.len(), 1);
949 assert_eq!(b.networks["1"].address, address(0xB));
950 }
951
952 fn hardhat_dir() -> PathBuf {
953 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
954 path.push("../examples/hardhat/deployments");
955 path
956 }
957
958 #[test]
959 fn load_from_directory() {
960 let artifact = HardHatLoader::new()
961 .load_from_directory(hardhat_dir())
962 .unwrap();
963
964 assert_eq!(artifact.len(), 1);
965
966 let a = artifact.get("DeployedContract").unwrap();
967 assert_eq!(a.name, "DeployedContract");
968 assert_eq!(a.networks.len(), 2);
969 assert_eq!(
970 a.networks["4"].address,
971 "0x4E29B76eC7d20c58A6B156CB464594a4ae39FdEd"
972 .parse()
973 .unwrap()
974 );
975 assert_eq!(
976 a.networks["4"].deployment_information,
977 Some(DeploymentInformation::TransactionHash(
978 "0x0122d15a8d394b8f9e45c15b7d3e5365bbf7122a15952246676e2fe7eb858f35"
979 .parse()
980 .unwrap()
981 ))
982 );
983 assert_eq!(
984 a.networks["1337"].address,
985 "0x29BE0588389993e7064C21f00761303eb51373F5"
986 .parse()
987 .unwrap()
988 );
989 assert_eq!(
990 a.networks["1337"].deployment_information,
991 Some(DeploymentInformation::TransactionHash(
992 "0xe0631d7f749fe73f94e59f6e25ff9b925980e8e29ed67b8f862ec76a783ea06e"
993 .parse()
994 .unwrap()
995 ))
996 );
997 }
998}