From 7d7a8bb707e59ab38618228c0dbda38dea1d7b09 Mon Sep 17 00:00:00 2001 From: cong-or <60357579+cong-or@users.noreply.github.com> Date: Thu, 4 Apr 2024 17:57:47 +0100 Subject: [PATCH 1/2] feat: index and capture registration data cip36 (#372) * feat(initial cddl parsing ): wip * fix(bump ci release) * ci * fix(bump ci release) * feat(cip36): reg capture * feat(cip36): reg capture * feat(cip36): reg capture * feat(cip36): reg capture * fix(ci): cspell * feat(initial cddl parsing ): wip * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * fix cspell * clippy lints * remove dalek * rm dalek * refactor(component bytes): struct docs * refactor(component bytes): struct docs * refactor(component bytes): struct docs * refactor(voting key as bytes): db entry --- .config/dictionaries/project.dic | 5 +- catalyst-gateway/Cargo.lock | 552 +++++++++++++++++- catalyst-gateway/Earthfile | 2 +- catalyst-gateway/bin/Cargo.toml | 2 + .../bin/src/event_db/legacy/types/event.rs | 16 +- .../bin/src/event_db/voter_registration.rs | 115 +++- catalyst-gateway/bin/src/follower.rs | 10 + catalyst-gateway/bin/src/main.rs | 1 + .../bin/src/registration/61284.cddl | 33 ++ .../bin/src/registration/61285.cddl | 12 + .../bin/src/registration/cip36.cddl | 40 ++ catalyst-gateway/bin/src/registration/mod.rs | 523 +++++++++++++++++ catalyst-gateway/bin/src/service/mod.rs | 4 +- catalyst-gateway/event-db/Earthfile | 10 +- .../event-db/migrations/V6__registration.sql | 11 +- .../insert_voter_registration.sql | 16 +- cspell.json | 77 +-- 17 files changed, 1300 insertions(+), 129 deletions(-) create mode 100644 catalyst-gateway/bin/src/registration/61284.cddl create mode 100644 catalyst-gateway/bin/src/registration/61285.cddl create mode 100644 catalyst-gateway/bin/src/registration/cip36.cddl create mode 100644 catalyst-gateway/bin/src/registration/mod.rs diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 4f18da3e913..623080d5f70 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -163,4 +163,7 @@ xcodeproj xctest xctestrun xcworkspace -yoroi \ No newline at end of file +yoroi +cbor +metamap +repr \ No newline at end of file diff --git a/catalyst-gateway/Cargo.lock b/catalyst-gateway/Cargo.lock index e5bf6ee948f..7173d614f0e 100644 --- a/catalyst-gateway/Cargo.lock +++ b/catalyst-gateway/Cargo.lock @@ -2,6 +2,37 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "abnf" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33741baa462d86e43fdec5e8ffca7c6ac82847ad06cbfb382c1bdbf527de9e6b" +dependencies = [ + "abnf-core", + "nom", +] + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom", +] + +[[package]] +name = "abnf_to_pest" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "939d59666dd9a7964a3a5312b9d24c9c107630752ee64f2dd5038189a23fe331" +dependencies = [ + "abnf", + "indexmap 1.9.3", + "itertools 0.10.5", + "pretty", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -156,6 +187,12 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.4" @@ -198,6 +235,17 @@ dependencies = [ "syn 2.0.53", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -264,6 +312,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + [[package]] name = "base58" version = "0.2.0" @@ -288,6 +342,15 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "base64-url" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb9fb9fb058cc3063b5fc88d9a21eefa2735871498a04e1650da76ed511c8569" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "bb8" version = "0.8.3" @@ -468,8 +531,10 @@ dependencies = [ "bb8", "bb8-postgres", "cardano-chain-follower", + "cddl", "chrono", - "clap", + "ciborium", + "clap 4.5.3", "cpu-time", "cryptoxide", "dotenvy", @@ -503,6 +568,39 @@ version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +[[package]] +name = "cddl" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc18488a72cef88de14f00d3db73f57a9511d53ae8dd72204a4bf8bc19309d7" +dependencies = [ + "abnf_to_pest", + "base16", + "base64-url", + "chrono", + "ciborium", + "clap 3.2.25", + "codespan-reporting", + "console_error_panic_hook", + "crossterm", + "data-encoding", + "displaydoc", + "hexf-parse", + "itertools 0.11.0", + "lexical-core", + "log", + "pest_meta", + "pest_vm", + "regex", + "regex-syntax 0.7.5", + "serde", + "serde-wasm-bindgen", + "serde_json", + "simplelog", + "uriparse", + "wasm-bindgen", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -529,6 +627,33 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half 2.4.0", +] + [[package]] name = "cipher" version = "0.4.4" @@ -539,6 +664,23 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_derive 3.2.25", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + [[package]] name = "clap" version = "4.5.3" @@ -546,7 +688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", - "clap_derive", + "clap_derive 4.5.3", ] [[package]] @@ -557,10 +699,23 @@ checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", - "clap_lex", + "clap_lex 0.7.0", "strsim 0.11.0", ] +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "clap_derive" version = "4.5.3" @@ -573,18 +728,47 @@ dependencies = [ "syn 2.0.53", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -658,6 +842,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -719,6 +934,12 @@ dependencies = [ "syn 2.0.53", ] +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + [[package]] name = "deranged" version = "0.3.11" @@ -752,6 +973,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -973,9 +1205,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -992,9 +1224,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", @@ -1015,6 +1247,16 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "half" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1066,6 +1308,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1078,6 +1329,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hkdf" version = "0.12.4" @@ -1183,7 +1440,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.25", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1206,7 +1463,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.3", + "h2 0.4.4", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1322,6 +1579,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1361,6 +1627,70 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.153" @@ -1445,7 +1775,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d15f4203d71fdf90903c2696e55426ac97a363c67b218488a73b534ce7aca10" dependencies = [ - "half", + "half 1.8.3", "minicbor-derive", ] @@ -1460,6 +1790,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1476,6 +1812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -1541,6 +1878,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1572,7 +1919,16 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ "libc", ] @@ -1652,6 +2008,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "overload" version = "0.1.1" @@ -1890,6 +2252,38 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_meta" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pest_vm" +version = "2.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d61488db5be4105b24ccbced8cf1d59408503589998a8593e692393e0af8d29" +dependencies = [ + "pest", + "pest_meta", +] + [[package]] name = "petgraph" version = "0.6.4" @@ -2152,6 +2546,18 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f3aa1e3ca87d3b124db7461265ac176b40c277f37e503eaa29c9c75c037846" +dependencies = [ + "arrayvec 0.5.2", + "log", + "typed-arena", + "unicode-segmentation", +] + [[package]] name = "prettyplease" version = "0.1.25" @@ -2180,6 +2586,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", + "syn 1.0.109", "version_check", ] @@ -2371,7 +2778,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax", + "regex-syntax 0.8.2", ] [[package]] @@ -2382,9 +2789,15 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -2478,7 +2891,7 @@ version = "1.34.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" dependencies = [ - "arrayvec", + "arrayvec 0.7.4", "borsh", "bytes", "num-traits", @@ -2566,6 +2979,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.197" @@ -2654,12 +3078,53 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "simdutf8" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2697,6 +3162,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.4" @@ -2787,6 +3258,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + [[package]] name = "thiserror" version = "1.0.58" @@ -2825,7 +3311,9 @@ checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -2982,7 +3470,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2 0.3.25", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", @@ -3108,12 +3596,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "ulid" version = "1.1.2" @@ -3166,6 +3666,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "universal-hash" version = "0.5.1" @@ -3182,6 +3694,16 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + [[package]] name = "url" version = "2.5.0" diff --git a/catalyst-gateway/Earthfile b/catalyst-gateway/Earthfile index 7d6b1a1a290..3f86bed54dc 100644 --- a/catalyst-gateway/Earthfile +++ b/catalyst-gateway/Earthfile @@ -4,7 +4,7 @@ VERSION --try --global-cache 0.7 # Set up our target toolchains, and copy our files. builder: - DO github.com/input-output-hk/catalyst-ci/earthly/rust:v2.10.1+SETUP + DO github.com/input-output-hk/catalyst-ci/earthly/rust:v2.10.3+SETUP COPY --dir .cargo .config Cargo.* clippy.toml deny.toml rustfmt.toml bin crates tests . COPY --dir ./event-db/queries ./event-db/queries diff --git a/catalyst-gateway/bin/Cargo.toml b/catalyst-gateway/bin/Cargo.toml index 467a8cec88a..055e22c9c7e 100644 --- a/catalyst-gateway/bin/Cargo.toml +++ b/catalyst-gateway/bin/Cargo.toml @@ -66,3 +66,5 @@ async-recursion = { workspace = true } pallas = { workspace = true } cardano-chain-follower= { workspace = true } anyhow = { workspace = true } +cddl = "0.9.2" +ciborium = "0.2" \ No newline at end of file diff --git a/catalyst-gateway/bin/src/event_db/legacy/types/event.rs b/catalyst-gateway/bin/src/event_db/legacy/types/event.rs index 12c7e9d25d1..e3c03f9b6c5 100644 --- a/catalyst-gateway/bin/src/event_db/legacy/types/event.rs +++ b/catalyst-gateway/bin/src/event_db/legacy/types/event.rs @@ -19,9 +19,9 @@ pub(crate) struct EventSummary { pub(crate) starts: Option>, /// Ends pub(crate) ends: Option>, - /// reg_checked + /// `reg_checked` pub(crate) reg_checked: Option>, - /// is_final + /// `is_final` pub(crate) is_final: bool, } @@ -69,17 +69,17 @@ pub(crate) struct EventGoal { #[derive(Debug, Clone, PartialEq, Eq)] /// Event Schedule pub(crate) struct EventSchedule { - /// insight_sharing + /// `insight_sharing` pub(crate) insight_sharing: Option>, - /// proposal_submission + /// `proposal_submission` pub(crate) proposal_submission: Option>, - /// refine_proposals + /// `refine_proposals` pub(crate) refine_proposals: Option>, - /// finalize_proposals + /// `finalize_proposals` pub(crate) finalize_proposals: Option>, - /// proposal_assessment + /// `proposal_assessment` pub(crate) proposal_assessment: Option>, - /// proposal_assessment_qa_start + /// `proposal_assessment_qa_start` pub(crate) assessment_qa_start: Option>, /// voting start pub(crate) voting: Option>, diff --git a/catalyst-gateway/bin/src/event_db/voter_registration.rs b/catalyst-gateway/bin/src/event_db/voter_registration.rs index b472af8b7d0..fe53ce9a461 100644 --- a/catalyst-gateway/bin/src/event_db/voter_registration.rs +++ b/catalyst-gateway/bin/src/event_db/voter_registration.rs @@ -1,6 +1,13 @@ //! Voter registration queries -use super::{Error, EventDB}; +use cardano_chain_follower::Network; +use pallas::ledger::traverse::MultiEraTx; +use serde_json::{json, Value}; + +use super::{follower::SlotNumber, Error, EventDB}; +use crate::registration::{ + parse_registrations_from_metadata, validate_reg_cddl, CddlConfig, Nonce as NonceReg, +}; /// Transaction id pub(crate) type TxId = String; @@ -13,20 +20,17 @@ pub(crate) type PaymentAddress<'a> = &'a [u8]; /// Nonce pub(crate) type Nonce = i64; /// Metadata 61284 -pub(crate) type Metadata61284<'a> = &'a [u8]; -/// Metadata 61285 -pub(crate) type Metadata61285<'a> = &'a [u8]; +pub(crate) type MetadataCip36<'a> = &'a [u8]; /// Stats -pub(crate) type Stats = Option; +pub(crate) type _Stats = Option; impl EventDB { /// Inserts voter registration data, replacing any existing data. #[allow(dead_code, clippy::too_many_arguments)] async fn insert_voter_registration( &self, tx_id: TxId, stake_credential: StakeCredential<'_>, - public_voting_key: PublicVotingKey<'_>, payment_address: PaymentAddress<'_>, nonce: Nonce, - metadata_61284: Metadata61284<'_>, metadata_61285: Metadata61285<'_>, valid: bool, - stats: Stats, + public_voting_key: PublicVotingKey<'_>, payment_address: PaymentAddress<'_>, + metadata_cip36: MetadataCip36<'_>, nonce: Nonce, report: Value, valid: bool, ) -> Result<(), Error> { let conn = self.pool.get().await?; @@ -41,14 +45,103 @@ impl EventDB { &public_voting_key, &payment_address, &nonce, - &metadata_61284, - &metadata_61285, + &metadata_cip36, + &report, &valid, - &stats, ], ) .await?; Ok(()) } + + /// Index registration data + pub async fn index_registration_data( + &self, txs: Vec>, slot_no: SlotNumber, network: Network, + ) -> Result<(), Error> { + let cddl = CddlConfig::new(); + + for tx in txs { + let mut valid_registration = true; + + if !tx.metadata().is_empty() { + let (registration, errors_report) = + match parse_registrations_from_metadata(&tx.metadata(), network) { + Ok(registration) => registration, + Err(_err) => { + // fatal error parsing registration tx, unable to extract meaningful + // errors assume corrupted tx + continue; + }, + }; + + // cddl verification + if let Some(cip36) = registration.clone().raw_cbor_cip36 { + match validate_reg_cddl(&cip36, &cddl) { + Ok(()) => (), + Err(_err) => { + // did not pass cddl verification, not a valid registration + continue; + }, + }; + } else { + // registration does not contain cip36 61284 or 61285 keys + // not a valid registration tx + continue; + } + + self.index_txn_data(tx.hash().as_slice(), slot_no, network) + .await?; + + let report = json!(&errors_report); + + if errors_report.is_empty() { + // valid registration + self.insert_voter_registration( + tx.hash().to_string(), + ®istration.stake_key.unwrap_or_default().0 .0, + serde_json::to_string(®istration.voting_key.unwrap_or_default()) + .unwrap_or_default() + .as_bytes(), + ®istration.rewards_address.unwrap_or_default().0, + ®istration.raw_cbor_cip36.unwrap_or_default(), + registration + .nonce + .unwrap_or(NonceReg(1)) + .0 + .try_into() + .unwrap_or(0), + report, + valid_registration, + ) + .await?; + } else { + // invalid registration + // index with invalid registration flag and error report + valid_registration = false; + + self.insert_voter_registration( + tx.hash().to_string(), + ®istration.stake_key.unwrap_or_default().0 .0, + serde_json::to_string(®istration.voting_key.unwrap_or_default()) + .unwrap_or_default() + .as_bytes(), + ®istration.rewards_address.unwrap_or_default().0, + ®istration.raw_cbor_cip36.unwrap_or_default(), + registration + .nonce + .unwrap_or(NonceReg(1)) + .0 + .try_into() + .unwrap_or(0), + report, + valid_registration, + ) + .await?; + } + } + } + + Ok(()) + } } diff --git a/catalyst-gateway/bin/src/follower.rs b/catalyst-gateway/bin/src/follower.rs index e9c15a2dca2..38e8163b2af 100644 --- a/catalyst-gateway/bin/src/follower.rs +++ b/catalyst-gateway/bin/src/follower.rs @@ -254,6 +254,16 @@ async fn init_follower( } // Registration + match db.index_registration_data(block.txs(), slot, network).await { + Ok(()) => (), + Err(err) => { + error!( + "Unable to index registration data for block {:?} - skip..", + err + ); + continue; + }, + } // Rewards } diff --git a/catalyst-gateway/bin/src/main.rs b/catalyst-gateway/bin/src/main.rs index 7bc631412c5..c658d782236 100644 --- a/catalyst-gateway/bin/src/main.rs +++ b/catalyst-gateway/bin/src/main.rs @@ -5,6 +5,7 @@ mod cli; mod event_db; mod follower; mod logger; +mod registration; mod service; mod settings; mod state; diff --git a/catalyst-gateway/bin/src/registration/61284.cddl b/catalyst-gateway/bin/src/registration/61284.cddl new file mode 100644 index 00000000000..f883f55cd13 --- /dev/null +++ b/catalyst-gateway/bin/src/registration/61284.cddl @@ -0,0 +1,33 @@ +registration_cbor = { + 61284: key_registration, +} + +$cip36_vote_pub_key /= bytes .size 32 +$payment_address /= bytes +$nonce /= uint +$weight /= uint .size 4 +$voting_purpose /= uint +legacy_key_registration = $cip36_vote_pub_key +delegation = [$cip36_vote_pub_key, $weight] + + +$stake_credential /= $staking_pub_key +$stake_witness /= $ed25519_signature +; A stake key credential, not tagged for backward compatibility +$staking_pub_key /= bytes .size 32 +; Witness for a stake key credential, not tagged for backward compatibility +$ed25519_signature /= bytes .size 64 + + +key_registration = { + 1 : [+delegation] / legacy_key_registration, + 2 : $stake_credential, + 3 : $payment_address, + 4 : $nonce, + ? 5 : $voting_purpose .default 0 +} + + +registration_witness = { + 1 : $stake_witness +} \ No newline at end of file diff --git a/catalyst-gateway/bin/src/registration/61285.cddl b/catalyst-gateway/bin/src/registration/61285.cddl new file mode 100644 index 00000000000..67f0d0d5f96 --- /dev/null +++ b/catalyst-gateway/bin/src/registration/61285.cddl @@ -0,0 +1,12 @@ +registration_cbor = { + 61285: registration_witness +} + +$stake_witness /= $ed25519_signature + +; Witness for a stake key credential, not tagged for backward compatibility +$ed25519_signature /= bytes .size 64 + +registration_witness = { + 1 : $stake_witness +} \ No newline at end of file diff --git a/catalyst-gateway/bin/src/registration/cip36.cddl b/catalyst-gateway/bin/src/registration/cip36.cddl new file mode 100644 index 00000000000..e30e9f87fca --- /dev/null +++ b/catalyst-gateway/bin/src/registration/cip36.cddl @@ -0,0 +1,40 @@ +registration_cbor = { + 61284: key_registration, + 61285: registration_witness, +} + +$cip36_vote_pub_key /= bytes .size 32 +$payment_address /= bytes +$nonce /= uint +$weight /= uint .size 4 +$voting_purpose /= uint +legacy_key_registration = $cip36_vote_pub_key +delegation = [$cip36_vote_pub_key, $weight] + + +$stake_credential /= $staking_pub_key +$stake_witness /= $ed25519_signature +; A stake key credential, not tagged for backward compatibility +$staking_pub_key /= bytes .size 32 +; Witness for a stake key credential, not tagged for backward compatibility +$ed25519_signature /= bytes .size 64 + + +key_registration = { + 1 : [+delegation] / legacy_key_registration, + 2 : $stake_credential, + 3 : $payment_address, + 4 : $nonce, + ? 5 : $voting_purpose .default 0 +} + + +registration_witness = { + 1 : $stake_witness +} + +$stake_witness /= $ed25519_signature + +; Witness for a stake key credential, not tagged for backward compatibility +$ed25519_signature /= bytes .size 64 + diff --git a/catalyst-gateway/bin/src/registration/mod.rs b/catalyst-gateway/bin/src/registration/mod.rs new file mode 100644 index 00000000000..ae7bfa1858a --- /dev/null +++ b/catalyst-gateway/bin/src/registration/mod.rs @@ -0,0 +1,523 @@ +//! Verify registration TXs + +use std::{error::Error, io::Cursor}; + +use cardano_chain_follower::Network; +use ciborium::Value; +use pallas::ledger::{primitives::Fragment, traverse::MultiEraMeta}; +use serde::{Deserialize, Serialize}; + +/// Networks +const NETWORK_ID: usize = 0; + +/// Cip36 - 61284 entries +const KEY_61284: usize = 0; + +/// Cip36 +const STAKE_ADDRESS: usize = 1; +/// Cip36 +const PAYMENT_ADDRESS: usize = 2; +/// Cip36 +const NONCE: usize = 3; +/// Cip36 +const VOTE_PURPOSE: usize = 4; +/// Cip36 +const DELEGATIONS_OR_DIRECT: usize = 0; +/// Cip36 +const VOTE_KEY: usize = 0; +/// Cip36 +const WEIGHT: usize = 1; + +/// +const CIP36_61284: usize = 61284; +/// +const CIP36_61285: usize = 61285; + +/// Pub key +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct PubKey(pub Vec); + +/// Nonce +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct Nonce(pub u64); + +/// Voting purpose +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct VotingPurpose(pub u64); + +/// Rewards address +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct RewardsAddress(pub Vec); + +/// Stake key +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct StakeKey(pub PubKey); + +/// Error report for serializing +pub type ErrorReport = Vec; + +/// Size of a single component of an Ed25519 signature. +const COMPONENT_SIZE: usize = 32; + +/// Size of an `R` or `s` component of an Ed25519 signature when serialized +/// as bytes. +pub type ComponentBytes = [u8; COMPONENT_SIZE]; + +/// Ed25519 signature serialized as a byte array. +pub type SignatureBytes = [u8; Signature::BYTE_SIZE]; + +/// Ed25519 signature. +/// +/// This type represents a container for the byte serialization of an Ed25519 +/// signature, and does not necessarily represent well-formed field or curve +/// elements. +/// +/// Signature verification libraries are expected to reject invalid field +/// elements at the time a signature is verified. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(C)] +pub struct Signature { + /// Component of an Ed25519 signature when serialized as bytes + r: ComponentBytes, + /// Component of an Ed25519 signature when serialized as bytes + s: ComponentBytes, +} + +impl Signature { + /// Size of an encoded Ed25519 signature in bytes. + pub const BYTE_SIZE: usize = COMPONENT_SIZE * 2; + + /// Parse an Ed25519 signature from a byte slice. + pub fn from_bytes(bytes: &SignatureBytes) -> Self { + let mut r = ComponentBytes::default(); + let mut s = ComponentBytes::default(); + + let components = bytes.split_at(COMPONENT_SIZE); + r.copy_from_slice(components.0); + s.copy_from_slice(components.1); + + Self { r, s } + } +} + +/// Cddl schema: +/// +pub struct CddlConfig { + /// Cip36 cddl representation + cip_36: String, +} + +impl CddlConfig { + #[must_use] + /// Create cddl config + pub fn new() -> Self { + let cip_36: String = include_str!("cip36.cddl").to_string(); + + CddlConfig { cip_36 } + } +} + +impl Default for CddlConfig { + fn default() -> Self { + Self::new() + } +} + +/// A catalyst registration on Cardano in either CIP-15 or CIP-36 format +#[derive(Debug, Clone, PartialEq)] +pub struct Registration { + /// Voting key + pub voting_key: Option, + /// Stake key + pub stake_key: Option, + /// Rewards address + pub rewards_address: Option, + /// Nonce + pub nonce: Option, + /// Optional voting purpose + pub voting_purpose: Option, + /// Raw cbor + pub raw_cbor_cip36: Option>, + /// Witness signature 61285 + pub signature: Option, +} + +/// The source of voting power for a given registration +/// +/// The voting power can either come from: +/// - a single wallet, OR +/// - a set of delegations +#[derive(Serialize, Deserialize)] +#[serde(untagged)] +#[derive(Debug, Clone, PartialEq)] +pub enum VotingKey { + /// Direct voting + /// + /// Voting power is based on the staked ada of the given key + Direct(PubKey), + + /// Delegated voting + /// + /// Voting power is based on the staked ada of the delegated keys + /// order of elements is important and must be preserved. + Delegated(Vec<(PubKey, u64)>), +} + +impl Default for VotingKey { + fn default() -> Self { + VotingKey::Direct(PubKey(Vec::new())) + } +} + +/// Validate raw registration binary against 61284 CDDL spec +/// +/// # Errors +/// +/// Failure will occur if parsed keys do not match CDDL spec +pub fn validate_reg_cddl(bin_reg: &[u8], cddl_config: &CddlConfig) -> Result<(), Box> { + cddl::validate_cbor_from_slice(&cddl_config.cip_36, bin_reg, None)?; + + Ok(()) +} + +/// Reward addresses start with a single header byte identifying their type and the +/// network, followed by 28 bytes of payload identifying either a stake key hash or a +/// script hash. Function accepts this first header prefix byte. +/// Validates first nibble is within the address range: 0x0? - 0x7? + 0xE? , 0xF? +/// Validates second nibble matches network id: 0/1 +#[must_use] +pub fn is_valid_rewards_address(rewards_address_prefix: u8, network: Network) -> bool { + let addr_type = rewards_address_prefix >> 4 & 0xF; + let addr_net = rewards_address_prefix & 0xF; + + // 0 or 1 are valid addrs in the following cases: + // type = 0x0 - Testnet network + // type = 0x1 - Mainnet network + match network { + Network::Mainnet => { + if addr_net != 1 { + return false; + } + }, + Network::Testnet => { + if addr_net != 0 { + return false; + } + }, + _ => (), + } + + // Valid addrs: 0x0?, 0x1?, 0x2?, 0x3?, 0x4?, 0x5?, 0x6?, 0x7?, 0xE?, 0xF?. + let valid_addrs = [0, 1, 2, 3, 4, 5, 6, 7, 14, 15]; + valid_addrs.contains(&addr_type) +} + +/// Convert raw 61285 cbor to witness signature +pub fn raw_sig_conversion(raw_cbor: &[u8]) -> Result> { + let decoded: ciborium::value::Value = ciborium::de::from_reader(Cursor::new(&raw_cbor))?; + + let signature_61285 = match decoded { + Value::Map(m) => m.iter().map(|entry| entry.1.clone()).collect::>(), + _ => return Err(format!("Invalid signature {decoded:?}").into()), + }; + + let sig = signature_61285.first().ok_or("no 61285 key")?.clone(); + let sig_bytes: [u8; 64] = match sig.into_bytes() { + Ok(s) => { + match s.try_into() { + Ok(sig) => sig, + Err(err) => return Err(format!("Invalid signature length {err:?}").into()), + } + }, + Err(err) => return Err(format!("Invalid signature parsing {err:?}").into()), + }; + + Ok(Signature::from_bytes(&sig_bytes)) +} + +#[allow(clippy::manual_let_else)] +/// Parse cip36 registration tx +pub fn inspect_metamap_reg(spec_61284: &[Value]) -> Result<&Vec<(Value, Value)>, Box> { + let metamap = match &spec_61284 + .get(KEY_61284) + .ok_or("Issue parsing 61284 parent key")? + { + Value::Map(metamap) => metamap, + _ => { + return Err(format!( + "Invalid metamap {:?}", + spec_61284.get(KEY_61284).ok_or("Issue parsing metamap")? + ) + .into()) + }, + }; + Ok(metamap) +} + +#[allow(clippy::manual_let_else)] +/// Extract voting key +pub fn inspect_voting_key(metamap: &[(Value, Value)]) -> Result> { + let voting_key = match &metamap + .get(DELEGATIONS_OR_DIRECT) + .ok_or("Issue with voting key 61284 cbor parsing")? + { + (Value::Integer(_one), Value::Bytes(direct)) => VotingKey::Direct(PubKey(direct.clone())), + (Value::Integer(_one), Value::Array(delegations)) => { + let mut delegations_map: Vec<(PubKey, u64)> = Vec::new(); + for d in delegations { + match d { + Value::Array(delegations) => { + let voting_key = match delegations + .get(VOTE_KEY) + .ok_or("Issue parsing delegations")? + .as_bytes() + { + Some(key) => key, + None => return Err("Invalid voting key".to_string().into()), + }; + + let weight = match delegations + .get(WEIGHT) + .ok_or("Issue parsing weight")? + .as_integer() + { + Some(weight) => { + match weight.try_into() { + Ok(weight) => weight, + Err(_err) => { + return Err("Invalid weight in delegation" + .to_string() + .into()) + }, + } + }, + None => return Err("Invalid delegation".to_string().into()), + }; + + delegations_map.push(((PubKey(voting_key.clone())), weight)); + }, + + _ => return Err("Invalid voting key".to_string().into()), + } + } + + VotingKey::Delegated(delegations_map) + }, + + _ => return Err("Invalid signature".to_string().into()), + }; + Ok(voting_key) +} + +/// Extract stake key +pub fn inspect_stake_key(metamap: &[(Value, Value)]) -> Result> { + let stake_key = match &metamap + .get(STAKE_ADDRESS) + .ok_or("Issue with stake key parsing")? + { + (Value::Integer(_two), Value::Bytes(stake_addr)) => PubKey(stake_addr.clone()), + _ => return Err("Invalid stake key".to_string().into()), + }; + Ok(stake_key) +} + +/// Extract and validate rewards address +pub fn inspect_rewards_addr( + metamap: &[(Value, Value)], network_id: Network, +) -> Result<&Vec, Box> { + let (Value::Integer(_three), Value::Bytes(rewards_address)) = &metamap + .get(PAYMENT_ADDRESS) + .ok_or("Issue with rewards address parsing")? + else { + return Err("Invalid rewards address".to_string().into()); + }; + + if !is_valid_rewards_address(*rewards_address.get(NETWORK_ID).ok_or("err")?, network_id) { + return Err("Invalid reward address".to_string().into()); + } + Ok(rewards_address) +} + +#[allow(clippy::indexing_slicing)] +/// Extract Nonce +pub fn inspect_nonce(metamap: &[(Value, Value)]) -> Result> { + let nonce = match metamap[NONCE] { + (Value::Integer(_four), Value::Integer(nonce)) => Nonce(nonce.try_into()?), + _ => return Err("Invalid nonce".to_string().into()), + }; + Ok(nonce) +} + +#[allow(clippy::indexing_slicing)] +/// Extract optional voting purpose +pub fn inspect_voting_purpose( + metamap: &[(Value, Value)], +) -> Result, Box> { + if metamap.len() == 5 { + match metamap[VOTE_PURPOSE] { + (Value::Integer(_five), Value::Integer(purpose)) => { + Ok(Some(VotingPurpose(purpose.try_into()?))) + }, + _ => Ok(None), + } + } else { + Ok(None) + } +} + +/// Extract registrations information for TX metadata +/// Collect secondary errors for granular json error report +pub fn parse_registrations_from_metadata( + meta: &MultiEraMeta, network: Network, +) -> Result<(Registration, ErrorReport), Box> { + let mut voting_key: Option = None; + let mut stake_key: Option = None; + let mut voting_purpose: Option = None; + let mut rewards_address: Option = None; + let mut nonce: Option = None; + let mut raw_cbor_cip36: Option> = None; + let mut sig: Option = None; + + let mut errors_report = Vec::new(); + + if let pallas::ledger::traverse::MultiEraMeta::AlonzoCompatible(meta) = meta { + for (key, cip36_registration) in meta.iter() { + if *key == u64::try_from(CIP36_61284)? { + let raw_cbor = meta.encode_fragment()?; + raw_cbor_cip36 = Some(raw_cbor.clone()); + + let decoded: ciborium::value::Value = + ciborium::de::from_reader(Cursor::new(&raw_cbor))?; + + let meta_61284 = if let Value::Map(m) = decoded { + m.iter().map(|entry| entry.1.clone()).collect::>() + } else { + errors_report.push(format!("61284 parent cddl invalid {decoded:?}")); + continue; + }; + + // 4 entries inside metadata map with one optional entry for the voting purpose + let metamap = match inspect_metamap_reg(&meta_61284) { + Ok(value) => value, + Err(err) => { + errors_report + .push(format!("61284 child cddl invalid {raw_cbor:?} {err:?}")); + continue; + }, + }; + + // voting key: simply an ED25519 public key. This is the spending credential in the + // side chain that will receive voting power from this delegation. + // For direct voting it's necessary to have the corresponding private key to cast + // votes in the side chain + match inspect_voting_key(metamap) { + Ok(value) => voting_key = Some(value), + Err(err) => { + voting_key = None; + errors_report.push(format!("Invalid voting key {raw_cbor:?} {err:?}")); + }, + }; + + // A stake address for the network that this transaction is submitted to (to point + // to the Ada that is being delegated); + match inspect_stake_key(metamap) { + Ok(value) => stake_key = Some(StakeKey(value)), + Err(err) => { + stake_key = None; + errors_report.push(format!("Invalid stake key {raw_cbor:?} {err:?}")); + }, + }; + + // A Shelley payment address (see CIP-0019) discriminated for the same network + // this transaction is submitted to, to receive rewards. + match inspect_rewards_addr(metamap, network) { + Ok(value) => rewards_address = Some(RewardsAddress(value.clone())), + Err(err) => { + rewards_address = None; + errors_report.push(format!("Invalid rewards address {raw_cbor:?} {err:?}")); + }, + }; + + // A nonce that identifies that most recent delegation + match inspect_nonce(metamap) { + Ok(value) => nonce = Some(value), + Err(err) => { + errors_report.push(format!("Invalid nonce {raw_cbor:?} {err:?}")); + nonce = None; + }, + }; + + // A non-negative integer that indicates the purpose of the vote. + // This is an optional field to allow for compatibility with CIP-15 + // 4 entries inside metadata map with one optional entry for the voting purpose + match inspect_voting_purpose(metamap) { + Ok(Some(value)) => voting_purpose = Some(value), + Ok(None) => voting_purpose = None, + Err(err) => { + voting_purpose = None; + errors_report.push(format!("Invalid voting purpose {raw_cbor:?} {err:?}")); + }, + }; + } else if *key == u64::try_from(CIP36_61285)? { + // Validate 61285 signature + let raw_cbor = cip36_registration.encode_fragment()?; + + match raw_sig_conversion(&cip36_registration.encode_fragment()?) { + Ok(signature) => { + sig = Some(signature); + }, + Err(err) => { + errors_report.push(format!( + "Invalid signature. cbor: {:?} {:?}", + hex::encode(raw_cbor), + err + )); + sig = None; + }, + }; + } + } + }; + + let r = Registration { + voting_key, + stake_key, + rewards_address, + nonce, + voting_purpose, + raw_cbor_cip36, + signature: sig, + }; + + Ok((r, errors_report)) +} + +#[cfg(test)] +#[test] +pub fn test_rewards_addr_permutations() { + // Valid addrs: 0x0?, 0x1?, 0x2?, 0x3?, 0x4?, 0x5?, 0x6?, 0x7?, 0xE?, 0xF?. + + let valid_addr_types = vec![0, 1, 2, 3, 4, 5, 6, 7, 14, 15]; + + for addr_type in valid_addr_types { + let test_addr = addr_type << 4; + assert!(is_valid_rewards_address(test_addr, Network::Testnet)); + assert!(!is_valid_rewards_address(test_addr, Network::Mainnet)); + + let test_addr = addr_type << 4 | 1; + assert!(!is_valid_rewards_address(test_addr, Network::Testnet)); + assert!(is_valid_rewards_address(test_addr, Network::Mainnet)); + } + + let invalid_addr_types = vec![8, 9, 10, 11, 12, 13]; + + for addr_type in invalid_addr_types { + let test_addr = addr_type << 4; + assert!(!is_valid_rewards_address(test_addr, Network::Testnet)); + assert!(!is_valid_rewards_address(test_addr, Network::Mainnet)); + + let test_addr = addr_type << 4 | 1; + assert!(!is_valid_rewards_address(test_addr, Network::Testnet)); + assert!(!is_valid_rewards_address(test_addr, Network::Mainnet)); + } +} diff --git a/catalyst-gateway/bin/src/service/mod.rs b/catalyst-gateway/bin/src/service/mod.rs index 99d40b881e8..1d001a710f0 100644 --- a/catalyst-gateway/bin/src/service/mod.rs +++ b/catalyst-gateway/bin/src/service/mod.rs @@ -17,13 +17,13 @@ pub(crate) use poem_service::get_app_docs; /// Service level errors #[derive(thiserror::Error, Debug)] pub(crate) enum Error { - /// An error with the EventDB + /// An error with the `EventDB` #[error(transparent)] EventDb(#[from] crate::event_db::error::Error), /// An IO error has occurred #[error(transparent)] Io(#[from] std::io::Error), - /// A mismatch in the expected EventDB schema version + /// A mismatch in the expected `EventDB` schema version #[error("expected schema version mismatch")] SchemaVersionMismatch, } diff --git a/catalyst-gateway/event-db/Earthfile b/catalyst-gateway/event-db/Earthfile index eb32b91bb5c..f01b26b1479 100644 --- a/catalyst-gateway/event-db/Earthfile +++ b/catalyst-gateway/event-db/Earthfile @@ -9,7 +9,7 @@ VERSION 0.7 # Internal: builder is our Event db builder target. Prepares all necessary artifacts. # CI target : dependency builder: - DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.0+BUILDER \ + DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.3+BUILDER \ --sqlfluff_cfg=./../../+repo-config/repo/.sqlfluff COPY ./../../+repo-config/repo/.sqlfluff . @@ -24,7 +24,7 @@ builder: check: FROM +builder - DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.0+CHECK + DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.3+CHECK # format all SQL files in the current project. Local developers tool. @@ -32,15 +32,15 @@ check: format: LOCALLY - DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.0+FORMAT --src=$(echo ${PWD}/../../) + DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.3+FORMAT --src=$(echo ${PWD}/../../) # build - an event db docker image. # CI target : true build: FROM +builder - DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.0+BUILD --image_name=event-db - DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.0+DOCS --image_name=event-db + DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.3+BUILD --image_name=event-db + DO github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.10.3+DOCS --image_name=event-db # test the event db database schema # CI target : true diff --git a/catalyst-gateway/event-db/migrations/V6__registration.sql b/catalyst-gateway/event-db/migrations/V6__registration.sql index 6dbce906c93..80637156753 100644 --- a/catalyst-gateway/event-db/migrations/V6__registration.sql +++ b/catalyst-gateway/event-db/migrations/V6__registration.sql @@ -292,11 +292,10 @@ CREATE TABLE cardano_voter_registration ( payment_address BYTEA NULL, nonce BIGINT NULL, - metadata_61284 BYTEA NULL, -- We can purge metadata for valid registrations that are old to save storage space. - metadata_61285 BYTEA NULL, -- We can purge metadata for valid registrations that are old to save storage space. + metadata_cip36 BYTEA NULL, -- We can purge metadata for valid registrations that are old to save storage space. valid BOOLEAN NOT NULL DEFAULT false, - stats JSONB NULL + stats JSONB -- record rolled back in stats if the registration was lost during a rollback, its also invalid at this point. -- Other stats we can record are is it a CIP-36 or CIP-15 registration format. -- does it have a valid reward address but not a payment address, so we can't pay to it. @@ -333,14 +332,10 @@ COMMENT ON COLUMN cardano_voter_registration.payment_address IS COMMENT ON COLUMN cardano_voter_registration.nonce IS 'The nonce of the registration. Registrations for the same stake address with higher nonces have priority.'; -COMMENT ON COLUMN cardano_voter_registration.metadata_61284 IS +COMMENT ON COLUMN cardano_voter_registration.metadata_cip36 IS 'The raw metadata for the CIP-15/36 registration. This data is optional, a parameter in config specifies how long raw registration metadata should be kept. Outside this time, the Registration record will be kept, but the raw metadata will be purged.'; -COMMENT ON COLUMN cardano_voter_registration.metadata_61285 IS -'The metadata for the CIP-15/36 registration signature. -This data is optional, a parameter in config specifies how long signature metadata should be kept. -Outside this time, the Registration record will be kept, but the signature metadata will be purged.'; COMMENT ON COLUMN cardano_voter_registration.valid IS 'True if the registration is valid, false if the registration is invalid. diff --git a/catalyst-gateway/event-db/queries/voter_registration/insert_voter_registration.sql b/catalyst-gateway/event-db/queries/voter_registration/insert_voter_registration.sql index ed3ece7a76b..b9fdb200bac 100644 --- a/catalyst-gateway/event-db/queries/voter_registration/insert_voter_registration.sql +++ b/catalyst-gateway/event-db/queries/voter_registration/insert_voter_registration.sql @@ -5,20 +5,18 @@ INSERT INTO cardano_voter_registration public_voting_key, payment_address, nonce, - metadata_61284, - metadata_61285, - valid, - stats + metadata_cip36, + stats, + valid ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (tx_id) DO UPDATE SET stake_credential = $2, public_voting_key = $3, payment_address = $4, nonce = $5, -metadata_61284 = $6, -metadata_61285 = $7, -valid = $8, -stats = $9; +metadata_cip36 = $6, +stats = $7, +valid = $8 diff --git a/cspell.json b/cspell.json index ec10d039055..b729343b8ba 100644 --- a/cspell.json +++ b/cspell.json @@ -124,98 +124,37 @@ } ], "dictionaries": [ - // Only enable extra languages as needed. "project-words", - // "ada", - // "aws", "bash", "companies", "cpp", "cryptocurrencies", - // "csharp", "css", "dart", "data-science", - // "django", "docker", - // "dotnet", - // "elixir", "filetypes", "fonts", - // "fsharp", "fullstack", - // "gaming-terms", "git", - // "golang", - // "haskell", "html", "html-symbol-entities", "java", - // "k8s", - // "latex", - // "lorem-ipsum", - // "lua", - // "node", - // "npm", - // "php", - // "powershell", "public-licenses", - // "python", - // "r", - // "ruby", "rust", - // "scala", "softwareTerms", "sql", - // "svelte", + "typescript", "swift", - "typescript" - // "vue", + "addrs", + "metamap", + "sidechain", + "permutations", + "cbor" ], "import": [ - // Only enable extra languages as needed. - //.config/dictionaries/cspell/al/cspell-ext.json - //.config/dictionaries/cspell/ar/cspell-ext.json - //.config/dictionaries/cspell/bg_BG/cspell-ext.json - //.config/dictionaries/cspell/ca/cspell-ext.json - //.config/dictionaries/cspell/city-names-finland/cspell-ext.json - //.config/dictionaries/cspell/cs_CZ/cspell-ext.json - //.config/dictionaries/cspell/da_DK/cspell-ext.json - //.config/dictionaries/cspell/de_AT/cspell-ext.json - //.config/dictionaries/cspell/de_CH/cspell-ext.json - //.config/dictionaries/cspell/de_DE/cspell-ext.json - //.config/dictionaries/cspell/el/cspell-ext.json - //.config/dictionaries/cspell/eo/cspell-ext.json - ".config/dictionaries/cspell/es_ES/cspell-ext.json", // Spanish - //.config/dictionaries/cspell/et-EE/cspell-ext.json - //.config/dictionaries/cspell/eu/cspell-ext.json - //.config/dictionaries/cspell/fa_IR/cspell-ext.json - //.config/dictionaries/cspell/fr_FR/cspell-ext.json - //.config/dictionaries/cspell/fr_FR_90/cspell-ext.json - //.config/dictionaries/cspell/he/cspell-ext.json - //.config/dictionaries/cspell/hr_HR/cspell-ext.json - //.config/dictionaries/cspell/it_IT/cspell-ext.json - //.config/dictionaries/cspell/lt_LT/cspell-ext.json - //.config/dictionaries/cspell/lv/cspell-ext.json - ".config/dictionaries/cspell/markdown/cspell-ext.json" // Markdown - //.config/dictionaries/cspell/mnemonics/cspell-ext.json - //.config/dictionaries/cspell/nb_NO/cspell-ext.json - //.config/dictionaries/cspell/nl_NL/cspell-ext.json - //.config/dictionaries/cspell/pl_PL/cspell-ext.json - //.config/dictionaries/cspell/pt_BR/cspell-ext.json - //.config/dictionaries/cspell/pt_PT/cspell-ext.json - //.config/dictionaries/cspell/redis/cspell-ext.json - //.config/dictionaries/cspell/ro_RO/cspell-ext.json - //.config/dictionaries/cspell/ru_RU/cspell-ext.json - //.config/dictionaries/cspell/scientific_terms_US/cspell-ext.json - //.config/dictionaries/cspell/shell/cspell-ext.json - //.config/dictionaries/cspell/sk_SK/cspell-ext.json - //.config/dictionaries/cspell/sr_Cyrl/cspell-ext.json - //.config/dictionaries/cspell/sr_Latn/cspell-ext.json - //.config/dictionaries/cspell/sv/cspell-ext.json - //.config/dictionaries/cspell/tr_TR/cspell-ext.json - //.config/dictionaries/cspell/uk_UA/cspell-ext.json - //.config/dictionaries/cspell/vi_VN/cspell-ext.json + ".config/dictionaries/cspell/es_ES/cspell-ext.json", + ".config/dictionaries/cspell/markdown/cspell-ext.json" ], "ignorePaths": [ ".config/dictionaries/**", From 41e8781d3c0e185470f6eaa5613d6886c7f41915 Mon Sep 17 00:00:00 2001 From: Lucio Baglione Date: Thu, 4 Apr 2024 19:46:03 +0200 Subject: [PATCH 2/2] feat: Add CatalystDataGatewayRepository (#376) * feat: Scaffold CatalystGateway repository. * feat: Add Cat Gateway active endpoints to repository. * test: CatalystDataGatewayRepository tests. * test: Complete tests for CatalystDataGatewayRepository. * docs: Specify how to generate mocks. * chore: Lint markdown. * chore: Add `mockito` as dev dep for CI tests. --- .../catalyst_voices_repositories/README.md | 22 ++ .../src/catalyst_data_gateway_repository.dart | 87 ++++++++ .../catalyst_voices_repositories/pubspec.yaml | 7 +- ...catalyst_data_gateway_repository_test.dart | 193 +++++++++++++++++ ...st_data_gateway_repository_test.mocks.dart | 201 ++++++++++++++++++ catalyst_voices/pubspec.yaml | 1 + 6 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart create mode 100644 catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart create mode 100644 catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.mocks.dart diff --git a/catalyst_voices/packages/catalyst_voices_repositories/README.md b/catalyst_voices/packages/catalyst_voices_repositories/README.md index 5316d7d4865..9ff8a84614a 100644 --- a/catalyst_voices/packages/catalyst_voices_repositories/README.md +++ b/catalyst_voices/packages/catalyst_voices_repositories/README.md @@ -1 +1,23 @@ # Catalyst Voices Repositories + +## Catalyst Data Gateway Repository + +### Tests + +When extending the `CatalystDataGatewayRepository` it is necessary to generate +proper mocks to have them available in tests. +To do that we need to run + +```sh +flutter pub run build_runner build --delete-conflicting-outputs +``` + + or + + ```sh +dart run build_runner build --delete-conflicting-outputs +``` + +in the Catalyst Voices Repositories package root. +The decorator `@GenerateNiceMocks` provided by mockito is used to indicate the +repository to generate the mocks for. diff --git a/catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart b/catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart new file mode 100644 index 00000000000..7dafda84579 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_repositories/lib/src/catalyst_data_gateway_repository.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.enums.swagger.dart' as enums; +import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.swagger.dart'; +import 'package:chopper/chopper.dart'; +import 'package:result_type/result_type.dart'; + +interface class CatalystDataGatewayRepository { + final CatGatewayApi _catGatewayApi; + + CatalystDataGatewayRepository(Uri baseUrl) + : _catGatewayApi = CatGatewayApi.create(baseUrl: baseUrl); + + Future> getHealthStarted() async { + try { + final heathStarted = await _catGatewayApi.apiHealthStartedGet(); + return _emptyBodyOrThrow(heathStarted); + } on ChopperHttpException catch (error) { + return Failure(_getNetworkError(error.response.statusCode)); + } + } + + Future> getHealthReady() async { + try { + final heathReady = await _catGatewayApi.apiHealthReadyGet(); + return _emptyBodyOrThrow(heathReady); + } on ChopperHttpException catch (error) { + return Failure(_getNetworkError(error.response.statusCode)); + } + } + + Future> getHealthLive() async { + try { + final healthLive = await _catGatewayApi.apiHealthLiveGet(); + return _emptyBodyOrThrow(healthLive); + } on ChopperHttpException catch (error) { + return Failure(_getNetworkError(error.response.statusCode)); + } + } + + Future> getCardanoStakedAdaStakeAddress({ + required String stakeAddress, + enums.Network network = enums.Network.mainnet, + DateTime? dateTime, + }) async { + try { + final stakeInfo = await _catGatewayApi.apiCardanoStakedAdaStakeAddressGet( + stakeAddress: stakeAddress, + network: network, + dateTime: dateTime, + ); + return Success(stakeInfo.bodyOrThrow); + } on ChopperHttpException catch (error) { + return Failure(_getNetworkError(error.response.statusCode)); + } + } + + Future> getCardanoSyncState({ + enums.Network network = enums.Network.mainnet, + }) async { + try { + final syncState = await _catGatewayApi.apiCardanoSyncStateGet( + network: network, + ); + return Success(syncState.bodyOrThrow); + } on ChopperHttpException catch (error) { + return Failure(_getNetworkError(error.response.statusCode)); + } + } + + NetworkErrors _getNetworkError(int statusCode) { + return NetworkErrors.values.firstWhere( + (error) => error.code == statusCode, + ); + } + + Result _emptyBodyOrThrow(Response response) { + // `bodyOrThrow` from chopper can't be used when the body is empty (like in + // case the endpoint replies with 204) because it would throw an exception + // as a false positive. + if (response.isSuccessful) { + return Success(null); + } + throw ChopperHttpException(response); + } +} diff --git a/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml index 2e9dc961f66..a2453a86a7f 100644 --- a/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_repositories/pubspec.yaml @@ -12,12 +12,15 @@ dependencies: path: ../catalyst_voices_models catalyst_voices_services: path: ../catalyst_voices_services + chopper: ^7.2.0 flutter: sdk: flutter result_type: ^0.2.0 rxdart: ^0.27.7 dev_dependencies: - catalyst_analysis: + build_runner: ^2.4.9 + catalyst_analysis: path: ../../../catalyst_voices_packages/catalyst_analysis - test: ^1.24.9 + mockito: ^5.4.4 + test: ^1.24.9 diff --git a/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart b/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart new file mode 100644 index 00000000000..faa941857e9 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.dart @@ -0,0 +1,193 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/catalyst_data_gateway_repository.dart'; +import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.swagger.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:result_type/result_type.dart'; +import 'package:test/test.dart'; + +import 'catalyst_data_gateway_repository_test.mocks.dart'; + + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('CatalystDataGatewayRepository', () { + final mock = MockCatalystDataGatewayRepository(); + test('getHealthStarted success', () async { + when(mock.getHealthStarted()).thenAnswer((_) async => Success(null)); + final result = await mock.getHealthStarted(); + expect(result.isSuccess, true); + }); + test('getHealthStarted Internal Server Error', () async { + when(mock.getHealthStarted()).thenAnswer((_) async { + return Failure(NetworkErrors.internalServerError); + }); + + final result = await mock.getHealthStarted(); + + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.internalServerError)); + }); + test('getHealthStarted Service Unavailable', () async { + when(mock.getHealthStarted()).thenAnswer((_) async { + return Failure(NetworkErrors.serviceUnavailable); + }); + final result = await mock.getHealthStarted(); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.serviceUnavailable)); + }); + + test('getHealthReady success', () async { + when(mock.getHealthReady()).thenAnswer((_) async => Success(null)); + final result = await mock.getHealthReady(); + expect(result.isSuccess, true); + }); + test('getHealthReady Internal Server Error', () async { + when(mock.getHealthReady()).thenAnswer((_) async { + return Failure(NetworkErrors.internalServerError); + }); + final result = await mock.getHealthReady(); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.internalServerError)); + }); + test('getHealthReady Service Unavailable', () async { + when(mock.getHealthReady()).thenAnswer((_) async { + return Failure(NetworkErrors.serviceUnavailable); + }); + final result = await mock.getHealthReady(); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.serviceUnavailable)); + }); + + test('getHealthLive success', () async { + when(mock.getHealthLive()).thenAnswer((_) async => Success(null)); + final result = await mock.getHealthLive(); + expect(result.isSuccess, true); + }); + test('getHealthLive Internal Server Error', () async { + when(mock.getHealthLive()).thenAnswer((_) async { + return Failure(NetworkErrors.internalServerError); + }); + final result = await mock.getHealthLive(); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.internalServerError)); + }); + test('getHealthLive Service Unavailable', () async { + when(mock.getHealthLive()).thenAnswer((_) async { + return Failure(NetworkErrors.serviceUnavailable); + }); + final result = await mock.getHealthLive(); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.serviceUnavailable)); + }); + + // cspell: disable + const validStakeAddress = + 'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw'; + // cspell: enable + const notValidStakeAddress = 'stake1wrong1stake'; + test('getCardanoStakedAdaStakeAddress success', () async { + final stakeInfo = StakeInfo( + amount: 1, + slotNumber: 5, + blockTime: DateTime.utc(1970), + ); + when( + mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ), + ).thenAnswer((_) async => Success(stakeInfo)); + final result = await mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ); + expect(result.isSuccess, true); + expect(result.success, equals(stakeInfo)); + }); + test('getCardanoStakedAdaStakeAddress Bad request', () async { + when( + mock.getCardanoStakedAdaStakeAddress( + stakeAddress: notValidStakeAddress, + ), + ).thenAnswer((_) async { + return Failure(NetworkErrors.badRequest); + }); + final result = await mock.getCardanoStakedAdaStakeAddress( + stakeAddress: notValidStakeAddress, + ); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.badRequest)); + }); + test('getCardanoStakedAdaStakeAddress Not found', () async { + when( + mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ), + ).thenAnswer((_) async { + return Failure(NetworkErrors.notFound); + }); + final result = await mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.notFound)); + }); + test('getCardanoStakedAdaStakeAddress Server Error', () async { + when( + mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ), + ).thenAnswer((_) async { + return Failure(NetworkErrors.internalServerError); + }); + final result = await mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.internalServerError)); + }); + + test('getCardanoStakedAdaStakeAddress Service Unavailable', () async { + when(mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ),).thenAnswer((_) async { + return Failure(NetworkErrors.serviceUnavailable); + }); + final result = await mock.getCardanoStakedAdaStakeAddress( + stakeAddress: validStakeAddress, + ); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.serviceUnavailable)); + }); + test('getCardanoSyncState success', () async { + final syncState = SyncState( + slotNumber: 5, + blockHash: + '0x0000000000000000000000000000000000000000000000000000000000000000', + lastUpdated: DateTime.utc(1970), + ); + when(mock.getCardanoSyncState()).thenAnswer( + (_) async => Success(syncState), + ); + final result = await mock.getCardanoSyncState(); + expect(result.isSuccess, true); + expect(result.success, equals(syncState)); + }); + test('getCardanoSyncState Server Error', () async { + when(mock.getCardanoSyncState()).thenAnswer( + (_) async => Failure(NetworkErrors.internalServerError), + ); + final result = await mock.getCardanoSyncState(); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.internalServerError)); + }); + test('getCardanoSyncState Service Unavailable', () async { + when(mock.getCardanoSyncState()).thenAnswer( + (_) async => Failure(NetworkErrors.serviceUnavailable), + ); + final result = await mock.getCardanoSyncState(); + expect(result.isFailure, true); + expect(result.failure, equals(NetworkErrors.serviceUnavailable)); + }); + + }); +} diff --git a/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.mocks.dart b/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.mocks.dart new file mode 100644 index 00000000000..b64461a2ded --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_repositories/test/src/catalyst_data_gateway_repository/catalyst_data_gateway_repository_test.mocks.dart @@ -0,0 +1,201 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in catalyst_voices_repositories/test/src/catalyst_data_gateway_repository_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart' as _i5; +import 'package:catalyst_voices_repositories/src/catalyst_data_gateway_repository.dart' + as _i3; +import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.enums.swagger.dart' + as _i7; +import 'package:catalyst_voices_services/generated/catalyst_gateway/cat_gateway_api.swagger.dart' + as _i6; +import 'package:mockito/mockito.dart' as _i1; +import 'package:result_type/result_type.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResult_0 extends _i1.SmartFake implements _i2.Result { + _FakeResult_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [CatalystDataGatewayRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCatalystDataGatewayRepository extends _i1.Mock + implements _i3.CatalystDataGatewayRepository { + @override + _i4.Future<_i2.Result> getHealthStarted() => + (super.noSuchMethod( + Invocation.method( + #getHealthStarted, + [], + ), + returnValue: _i4.Future<_i2.Result>.value( + _FakeResult_0( + this, + Invocation.method( + #getHealthStarted, + [], + ), + )), + returnValueForMissingStub: + _i4.Future<_i2.Result>.value( + _FakeResult_0( + this, + Invocation.method( + #getHealthStarted, + [], + ), + )), + ) as _i4.Future<_i2.Result>); + + @override + _i4.Future<_i2.Result> getHealthReady() => + (super.noSuchMethod( + Invocation.method( + #getHealthReady, + [], + ), + returnValue: _i4.Future<_i2.Result>.value( + _FakeResult_0( + this, + Invocation.method( + #getHealthReady, + [], + ), + )), + returnValueForMissingStub: + _i4.Future<_i2.Result>.value( + _FakeResult_0( + this, + Invocation.method( + #getHealthReady, + [], + ), + )), + ) as _i4.Future<_i2.Result>); + + @override + _i4.Future<_i2.Result> getHealthLive() => + (super.noSuchMethod( + Invocation.method( + #getHealthLive, + [], + ), + returnValue: _i4.Future<_i2.Result>.value( + _FakeResult_0( + this, + Invocation.method( + #getHealthLive, + [], + ), + )), + returnValueForMissingStub: + _i4.Future<_i2.Result>.value( + _FakeResult_0( + this, + Invocation.method( + #getHealthLive, + [], + ), + )), + ) as _i4.Future<_i2.Result>); + + @override + _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>> + getCardanoStakedAdaStakeAddress({ + required String? stakeAddress, + _i7.Network? network = _i7.Network.mainnet, + DateTime? dateTime, + }) => + (super.noSuchMethod( + Invocation.method( + #getCardanoStakedAdaStakeAddress, + [], + { + #stakeAddress: stakeAddress, + #network: network, + #dateTime: dateTime, + }, + ), + returnValue: + _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>>.value( + _FakeResult_0<_i6.StakeInfo, _i5.NetworkErrors>( + this, + Invocation.method( + #getCardanoStakedAdaStakeAddress, + [], + { + #stakeAddress: stakeAddress, + #network: network, + #dateTime: dateTime, + }, + ), + )), + returnValueForMissingStub: + _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>>.value( + _FakeResult_0<_i6.StakeInfo, _i5.NetworkErrors>( + this, + Invocation.method( + #getCardanoStakedAdaStakeAddress, + [], + { + #stakeAddress: stakeAddress, + #network: network, + #dateTime: dateTime, + }, + ), + )), + ) as _i4.Future<_i2.Result<_i6.StakeInfo, _i5.NetworkErrors>>); + + @override + _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>> getCardanoSyncState( + {_i7.Network? network = _i7.Network.mainnet}) => + (super.noSuchMethod( + Invocation.method( + #getCardanoSyncState, + [], + {#network: network}, + ), + returnValue: + _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>>.value( + _FakeResult_0<_i6.SyncState, _i5.NetworkErrors>( + this, + Invocation.method( + #getCardanoSyncState, + [], + {#network: network}, + ), + )), + returnValueForMissingStub: + _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>>.value( + _FakeResult_0<_i6.SyncState, _i5.NetworkErrors>( + this, + Invocation.method( + #getCardanoSyncState, + [], + {#network: network}, + ), + )), + ) as _i4.Future<_i2.Result<_i6.SyncState, _i5.NetworkErrors>>); +} diff --git a/catalyst_voices/pubspec.yaml b/catalyst_voices/pubspec.yaml index 130a51b8721..8eb65dd770d 100644 --- a/catalyst_voices/pubspec.yaml +++ b/catalyst_voices/pubspec.yaml @@ -52,6 +52,7 @@ dev_dependencies: go_router_builder: ^2.4.1 integration_test: sdk: flutter + mockito: ^5.4.4 mocktail: ^1.0.1 flutter: