diff --git a/.gitignore b/.gitignore index b02f7f66..b1affaa1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -tarpaulin-report.* \ No newline at end of file +tarpaulin-report.* +Cargo.lock \ No newline at end of file diff --git a/.tarpaulin.toml b/.tarpaulin.toml index a1529758..a87a0335 100644 --- a/.tarpaulin.toml +++ b/.tarpaulin.toml @@ -1,5 +1,9 @@ [all] -exclude-files = ["src/types/sargon_types.rs", "src/testing/*", "src/samples/*"] +exclude-files = [ + "crates/rules-uniffi/src/unneeded_when_moved_to_sargon.rs", + "crates/rules-uniffi/src/error_conversion.rs", + "crates/rules/src/move_to_sargon.rs", +] verbose = false force-clean = true timeout = "2m" diff --git a/.vscode/settings.json b/.vscode/settings.json index f01e03c0..6d13a41c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "Appendable", "Banksy", "bdfs", + "fsid", "Fulfillable", "interactor", "Interactors", diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 4898266c..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,729 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" -dependencies = [ - "anstyle", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" - -[[package]] -name = "askama" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" -dependencies = [ - "askama_derive", - "askama_escape", -] - -[[package]] -name = "askama_derive" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" -dependencies = [ - "askama_parser", - "basic-toml", - "mime", - "mime_guess", - "proc-macro2", - "quote", - "serde", - "syn", -] - -[[package]] -name = "askama_escape" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" - -[[package]] -name = "askama_parser" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" -dependencies = [ - "nom", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "basic-toml" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" -dependencies = [ - "serde", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bytes" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" - -[[package]] -name = "camino" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo-platform" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "clap" -version = "4.5.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" - -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "fs-err" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" -dependencies = [ - "autocfg", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "goblin" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" -dependencies = [ - "log", - "plain", - "scroll", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[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 = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "proc-macro2" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rules" -version = "0.1.0" -dependencies = [ - "thiserror 2.0.3", -] - -[[package]] -name = "rules-uniffi" -version = "0.1.0" -dependencies = [ - "rules", - "thiserror 2.0.3", - "uniffi", -] - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "scroll" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" -dependencies = [ - "serde", -] - -[[package]] -name = "serde" -version = "1.0.215" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.215" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.133" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "textwrap" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" -dependencies = [ - "smawk", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" -dependencies = [ - "thiserror-impl 2.0.3", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "unicase" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" - -[[package]] -name = "unicode-ident" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" - -[[package]] -name = "uniffi" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "anyhow", - "camino", - "clap", - "uniffi_bindgen", - "uniffi_build", - "uniffi_core", - "uniffi_macros", -] - -[[package]] -name = "uniffi_bindgen" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "anyhow", - "askama", - "camino", - "cargo_metadata", - "clap", - "fs-err", - "glob", - "goblin", - "heck", - "once_cell", - "paste", - "serde", - "textwrap", - "toml", - "uniffi_meta", - "uniffi_testing", - "uniffi_udl", -] - -[[package]] -name = "uniffi_build" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "anyhow", - "camino", - "uniffi_bindgen", -] - -[[package]] -name = "uniffi_checksum_derive" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "uniffi_core" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "anyhow", - "bytes", - "camino", - "log", - "once_cell", - "paste", - "static_assertions", -] - -[[package]] -name = "uniffi_macros" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "bincode", - "camino", - "fs-err", - "once_cell", - "proc-macro2", - "quote", - "serde", - "syn", - "toml", - "uniffi_meta", -] - -[[package]] -name = "uniffi_meta" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "anyhow", - "bytes", - "siphasher", - "uniffi_checksum_derive", -] - -[[package]] -name = "uniffi_testing" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "anyhow", - "camino", - "cargo_metadata", - "fs-err", - "once_cell", -] - -[[package]] -name = "uniffi_udl" -version = "0.27.1" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "anyhow", - "textwrap", - "uniffi_meta", - "uniffi_testing", - "weedle2", -] - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "weedle2" -version = "5.0.0" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=6f33088e8100a2ea9586c8c3ecf98ab51d5aba62#6f33088e8100a2ea9586c8c3ecf98ab51d5aba62" -dependencies = [ - "nom", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 770dff92..4874ba61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,4 @@ debug = true [workspace.dependencies] thiserror = "2.0.3" +sargon = { git = "https://github.com/radixdlt/sargon", tag = "1.1.70" } diff --git a/README.md b/README.md index 14adf54b..981343ea 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -With https://github.com/radixdlt/sargon/pull/254 merged this repo is now archived. +# Rules + +[Implementation of FactorSourceID rules defined here][doc] + +[doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields diff --git a/crates/rules-uniffi/Cargo.toml b/crates/rules-uniffi/Cargo.toml index 2eeb8a5a..89c91eca 100644 --- a/crates/rules-uniffi/Cargo.toml +++ b/crates/rules-uniffi/Cargo.toml @@ -2,18 +2,23 @@ name = "rules-uniffi" version = "0.1.0" edition = "2021" +build = "build.rs" +[lib] +crate-type = ["staticlib", "cdylib", "lib"] [dependencies] +sargon = { workspace = true } rules = { path = "../rules" } +thiserror = { workspace = true } +serde = { version = "1.0.215", features = ["derive"] } +pretty_assertions = "1.4.1" # uniffi = "0.27.1" uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "6f33088e8100a2ea9586c8c3ecf98ab51d5aba62", features = [ "cli", ] } -thiserror = { workspace = true } - [dev-dependencies] # uniffi = "0.27.1" uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "6f33088e8100a2ea9586c8c3ecf98ab51d5aba62", features = [ diff --git a/crates/rules-uniffi/build.rs b/crates/rules-uniffi/build.rs new file mode 100644 index 00000000..0f27335e --- /dev/null +++ b/crates/rules-uniffi/build.rs @@ -0,0 +1,3 @@ +pub fn main() { + uniffi::generate_scaffolding("src/sargon.udl").expect("Should be able to build."); +} diff --git a/crates/rules-uniffi/src/builder.rs b/crates/rules-uniffi/src/builder.rs new file mode 100644 index 00000000..d8ce6fd0 --- /dev/null +++ b/crates/rules-uniffi/src/builder.rs @@ -0,0 +1,497 @@ +#![allow(clippy::new_without_default)] +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::sync::{Arc, RwLock}; + +use sargon::IndexSet; + +use crate::prelude::*; + +#[derive(Debug, uniffi::Object)] +pub struct SecurityShieldBuilder { + wrapped: RwLock>, + name: RwLock, +} + +#[derive(Debug, PartialEq, Eq, Hash, uniffi::Object)] +#[uniffi::export(Debug, Eq, Hash)] +pub struct SecurityStructureOfFactorSourceIds { + pub wrapped: rules::SecurityStructureOfFactorSourceIds, +} + +impl SecurityShieldBuilder { + fn get(&self, with_non_consumed_builder: impl Fn(&MatrixBuilder) -> R) -> R { + let binding = self.wrapped.read().unwrap(); + + let Some(builder) = binding.as_ref() else { + unreachable!("Already built, should not have happened.") + }; + with_non_consumed_builder(builder) + } + + fn with>( + &self, + mut with_non_consumed_builder: impl FnMut(&mut MatrixBuilder) -> Result, + ) -> Result { + let guard = self.wrapped.write(); + + let mut binding = guard.map_err(|_| CommonError::MatrixBuilderRwLockPoisoned)?; + + let Some(builder) = binding.as_mut() else { + return Err(CommonError::AlreadyBuilt); + }; + with_non_consumed_builder(builder).map_err(|e| Into::::into(e)) + } + + fn validation_for_addition_of_factor_source_by_calling( + &self, + factor_sources: Vec>, + call: impl Fn( + &MatrixBuilder, + &IndexSet, + ) -> IndexSet, + ) -> Result>, CommonError> { + let input = &factor_sources + .clone() + .into_iter() + .map(|x| x.inner) + .collect::>(); + self.with(|builder| { + let xs = call(builder, input); + + let xs = xs + .into_iter() + .map(Into::::into) + .map(Arc::new) + .collect(); + + Ok::<_, CommonError>(xs) + }) + } +} + +#[uniffi::export] +impl SecurityShieldBuilder { + #[uniffi::constructor] + pub fn new() -> Arc { + Arc::new(Self { + wrapped: RwLock::new(Some(MatrixBuilder::new())), + name: RwLock::new("My Shield".to_owned()), + }) + } +} + +impl SecurityShieldBuilder { + fn get_factors( + &self, + access: impl Fn(&MatrixBuilder) -> &Vec, + ) -> Vec> { + self.get(|builder| { + let factors = access(builder); + factors.iter().map(FactorSourceID::new).collect::>() + }) + } +} + +// ==================== +// ==== GET / READ ==== +// ==================== +#[uniffi::export] +impl SecurityShieldBuilder { + pub fn get_primary_threshold(&self) -> u8 { + self.get(|builder| builder.get_threshold()) + } + + pub fn get_number_of_days_until_auto_confirm(&self) -> u16 { + self.get(|builder| builder.get_number_of_days_until_auto_confirm()) + } + + pub fn get_name(&self) -> String { + self.name.read().unwrap().clone() + } + + pub fn get_primary_threshold_factors(&self) -> Vec> { + self.get_factors(|builder| builder.get_primary_threshold_factors()) + } + + pub fn get_primary_override_factors(&self) -> Vec> { + self.get_factors(|builder| builder.get_primary_override_factors()) + } + + pub fn get_recovery_factors(&self) -> Vec> { + self.get_factors(|builder| builder.get_recovery_factors()) + } + + pub fn get_confirmation_factors(&self) -> Vec> { + self.get_factors(|builder| builder.get_confirmation_factors()) + } +} + +// ==================== +// ===== MUTATION ===== +// ==================== +#[uniffi::export] +impl SecurityShieldBuilder { + pub fn set_name(&self, name: String) { + *self.name.write().unwrap() = name + } + + /// Adds the factor source to the primary role threshold list. + pub fn add_factor_source_to_primary_threshold( + &self, + factor_source_id: Arc, + ) -> Result<(), CommonError> { + self.with(|builder| builder.add_factor_source_to_primary_threshold(factor_source_id.inner)) + } + + pub fn add_factor_source_to_primary_override( + &self, + factor_source_id: Arc, + ) -> Result<(), CommonError> { + self.with(|builder| builder.add_factor_source_to_primary_override(factor_source_id.inner)) + } + + pub fn remove_factor(&self, factor_source_id: Arc) -> Result<(), CommonError> { + self.with(|builder| builder.remove_factor(&factor_source_id.inner)) + } + + pub fn set_threshold(&self, threshold: u8) -> Result<(), CommonError> { + self.with(|builder| builder.set_threshold(threshold)) + } + + pub fn set_number_of_days_until_auto_confirm( + &self, + number_of_days: u16, + ) -> Result<(), CommonError> { + self.with(|builder| builder.set_number_of_days_until_auto_confirm(number_of_days)) + } + + pub fn add_factor_source_to_recovery_override( + &self, + factor_source_id: Arc, + ) -> Result<(), CommonError> { + self.with(|builder| builder.add_factor_source_to_recovery_override(factor_source_id.inner)) + } + + pub fn add_factor_source_to_confirmation_override( + &self, + factor_source_id: Arc, + ) -> Result<(), CommonError> { + self.with(|builder| { + builder.add_factor_source_to_confirmation_override(factor_source_id.inner) + }) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> Result<(), CommonError> { + self.with(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + factor_source_kind.into(), + ) + }) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_recovery_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> Result<(), CommonError> { + self.with(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_recovery_override( + factor_source_kind.into(), + ) + }) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_primary_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> Result<(), CommonError> { + self.with(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_primary_override( + factor_source_kind.into(), + ) + }) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + &self, + factor_source_kind: FactorSourceKind, + ) -> Result<(), CommonError> { + self.with(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + factor_source_kind.into(), + ) + }) + } + + pub fn validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &self, + factor_sources: Vec>, + ) -> Result>, CommonError> { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder + .validation_for_addition_of_factor_source_to_primary_threshold_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_primary_override_for_each( + &self, + factor_sources: Vec>, + ) -> Result>, CommonError> { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder.validation_for_addition_of_factor_source_to_primary_override_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_recovery_override_for_each( + &self, + factor_sources: Vec>, + ) -> Result>, CommonError> { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder + .validation_for_addition_of_factor_source_to_recovery_override_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &self, + factor_sources: Vec>, + ) -> Result>, CommonError> { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder.validation_for_addition_of_factor_source_to_confirmation_override_for_each( + input, + ) + }, + ) + } + + pub fn build(self: Arc) -> Result { + let mut binding = self + .wrapped + .write() + .map_err(|_| CommonError::MatrixBuilderRwLockPoisoned)?; + let builder = binding.take().ok_or(CommonError::AlreadyBuilt)?; + let wrapped_matrix = builder + .build() + .map_err(|e| CommonError::BuildError(format!("{:?}", e)))?; + + let name = self.get_name(); + let display_name = + sargon::DisplayName::new(name).map_err(|e| CommonError::Sargon(format!("{:?}", e)))?; + let wrapped_shield = + rules::SecurityStructureOfFactorSourceIds::new(display_name, wrapped_matrix); + + let shield = SecurityStructureOfFactorSourceIds { + wrapped: wrapped_shield, + }; + Ok(shield) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityShieldBuilder; + + #[test] + fn test() { + let sut = SUT::new(); + + assert_eq!(sut.get_name(), "My Shield"); + sut.set_name("S.H.I.E.L.D.".to_owned()); + + assert_eq!(sut.get_number_of_days_until_auto_confirm(), 14); + sut.set_number_of_days_until_auto_confirm(u16::MAX).unwrap(); + assert_eq!(sut.get_number_of_days_until_auto_confirm(), u16::MAX); + + // Primary + let sim_prim = + sut.validation_for_addition_of_factor_source_to_primary_override_for_each(vec![ + FactorSourceID::sample_arculus(), + ]); + + let sim_prim_threshold = sut + .validation_for_addition_of_factor_source_to_primary_threshold_for_each(vec![ + FactorSourceID::sample_arculus(), + ]); + + let sim_kind_prim = sut + .validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + + let sim_kind_prim_threshold = sut + .validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + assert_eq!( + sut.get_primary_threshold_factors(), + vec![FactorSourceID::sample_device()] + ); + _ = sut.set_threshold(1); + assert_eq!(sut.get_primary_threshold(), 1); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus()) + .unwrap(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus_other()) + .unwrap(); + + assert_eq!( + sut.get_primary_override_factors(), + vec![ + FactorSourceID::sample_arculus(), + FactorSourceID::sample_arculus_other() + ] + ); + + // Recovery + let sim_rec = + sut.validation_for_addition_of_factor_source_to_recovery_override_for_each(vec![ + FactorSourceID::sample_ledger(), + ]); + + let sim_kind_rec = sut + .validation_for_addition_of_factor_source_of_kind_to_recovery_override( + FactorSourceKind::ArculusCard, + ); + + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + assert_eq!( + sut.get_recovery_factors(), + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other() + ] + ); + + // Confirmation + let sim_conf = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each(vec![ + FactorSourceID::sample_device(), + ]); + + let sim_kind_conf = sut + .validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + FactorSourceKind::ArculusCard, + ); + + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + assert_eq!( + sut.get_confirmation_factors(), + vec![FactorSourceID::sample_device(),] + ); + + assert_ne!( + sim_prim, + sut.validation_for_addition_of_factor_source_to_primary_override_for_each(vec![ + FactorSourceID::sample_arculus(), + ]) + ); + + assert_ne!( + sim_prim_threshold, + sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each(vec![ + FactorSourceID::sample_arculus() + ]) + ); + + assert_ne!( + sim_rec, + sut.validation_for_addition_of_factor_source_to_recovery_override_for_each(vec![ + FactorSourceID::sample_ledger(), + ]) + ); + + assert_ne!( + sim_conf, + sut.validation_for_addition_of_factor_source_to_confirmation_override_for_each(vec![ + FactorSourceID::sample_device(), + ]) + ); + + assert_ne!( + sim_kind_prim, + sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ) + ); + + assert_ne!( + sim_kind_prim_threshold, + sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ) + ); + + assert_eq!( + sim_kind_rec, + sut.validation_for_addition_of_factor_source_of_kind_to_recovery_override( + FactorSourceKind::ArculusCard, + ) + ); + + assert_eq!( + sim_kind_conf, + sut.validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + FactorSourceKind::ArculusCard, + ) + ); + + sut.remove_factor(FactorSourceID::sample_arculus_other()) + .unwrap(); + sut.remove_factor(FactorSourceID::sample_ledger_other()) + .unwrap(); + + let shield = sut.build().unwrap(); + assert_eq!(shield.wrapped.metadata.display_name.value, "S.H.I.E.L.D."); + assert_eq!( + shield + .wrapped + .matrix_of_factors + .primary() + .get_override_factors(), + &vec![FactorSourceID::sample_arculus().inner] + ); + assert_eq!( + shield + .wrapped + .matrix_of_factors + .recovery() + .get_override_factors(), + &vec![FactorSourceID::sample_ledger().inner] + ); + assert_eq!( + shield + .wrapped + .matrix_of_factors + .confirmation() + .get_override_factors(), + &vec![FactorSourceID::sample_device().inner] + ); + } +} diff --git a/crates/rules-uniffi/src/error_conversion.rs b/crates/rules-uniffi/src/error_conversion.rs new file mode 100644 index 00000000..7022a621 --- /dev/null +++ b/crates/rules-uniffi/src/error_conversion.rs @@ -0,0 +1,28 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] +pub enum CommonError { + #[error("Sargon")] + Sargon(String), + + #[error("AlreadyBuilt")] + AlreadyBuilt, + + #[error("Matrix builder RwLock poisoned")] + MatrixBuilderRwLockPoisoned, + + #[error("Build error {0}")] + BuildError(String), +} + +impl From for CommonError { + fn from(val: MatrixBuilderValidation) -> Self { + CommonError::BuildError(format!("{:?}", val)) + } +} + +impl From for CommonError { + fn from(val: RoleBuilderValidation) -> Self { + CommonError::BuildError(format!("{:?}", val)) + } +} diff --git a/crates/rules-uniffi/src/lib.rs b/crates/rules-uniffi/src/lib.rs index e69de29b..777df2fb 100644 --- a/crates/rules-uniffi/src/lib.rs +++ b/crates/rules-uniffi/src/lib.rs @@ -0,0 +1,14 @@ +mod builder; +mod error_conversion; +mod models; +mod unneeded_when_moved_to_sargon; + +pub mod prelude { + pub(crate) use rules::prelude::*; + + pub(crate) use crate::error_conversion::*; + pub(crate) use crate::models::*; + pub(crate) use crate::unneeded_when_moved_to_sargon::*; +} + +uniffi::include_scaffolding!("sargon"); diff --git a/crates/rules-uniffi/src/models/factor_source_in_role_builder_validation_status.rs b/crates/rules-uniffi/src/models/factor_source_in_role_builder_validation_status.rs new file mode 100644 index 00000000..51f1c747 --- /dev/null +++ b/crates/rules-uniffi/src/models/factor_source_in_role_builder_validation_status.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone, PartialEq, Eq, Hash, uniffi::Object)] +pub struct FactorSourceValidationStatus { + pub role: sargon::RoleKind, + pub factor_source_id: sargon::FactorSourceID, + pub validation: rules::RoleBuilderMutateResult, +} + +impl From for FactorSourceValidationStatus { + fn from(val: rules::FactorSourceInRoleBuilderValidationStatus) -> Self { + FactorSourceValidationStatus { + role: val.role, + factor_source_id: val.factor_source_id, + validation: val.validation, + } + } +} diff --git a/crates/rules-uniffi/src/models/mod.rs b/crates/rules-uniffi/src/models/mod.rs new file mode 100644 index 00000000..81c8aa5a --- /dev/null +++ b/crates/rules-uniffi/src/models/mod.rs @@ -0,0 +1,3 @@ +mod factor_source_in_role_builder_validation_status; + +pub use factor_source_in_role_builder_validation_status::*; diff --git a/crates/rules-uniffi/src/sargon.udl b/crates/rules-uniffi/src/sargon.udl new file mode 100644 index 00000000..fa3390fb --- /dev/null +++ b/crates/rules-uniffi/src/sargon.udl @@ -0,0 +1,2 @@ + +namespace sargon {}; \ No newline at end of file diff --git a/crates/rules-uniffi/src/unneeded_when_moved_to_sargon.rs b/crates/rules-uniffi/src/unneeded_when_moved_to_sargon.rs new file mode 100644 index 00000000..1e36c6ff --- /dev/null +++ b/crates/rules-uniffi/src/unneeded_when_moved_to_sargon.rs @@ -0,0 +1,69 @@ +use std::{borrow::Borrow, sync::Arc}; + +#[cfg(test)] +use rules::SampleValues; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum FactorSourceKind { + Device, + LedgerHQHardwareWallet, + Password, + OffDeviceMnemonic, + TrustedContact, + SecurityQuestions, + ArculusCard, +} +impl From for sargon::FactorSourceKind { + fn from(value: FactorSourceKind) -> Self { + match value { + FactorSourceKind::Device => sargon::FactorSourceKind::Device, + FactorSourceKind::LedgerHQHardwareWallet => { + sargon::FactorSourceKind::LedgerHQHardwareWallet + } + FactorSourceKind::Password => sargon::FactorSourceKind::Password, + FactorSourceKind::OffDeviceMnemonic => sargon::FactorSourceKind::OffDeviceMnemonic, + FactorSourceKind::TrustedContact => sargon::FactorSourceKind::TrustedContact, + FactorSourceKind::SecurityQuestions => sargon::FactorSourceKind::SecurityQuestions, + FactorSourceKind::ArculusCard => sargon::FactorSourceKind::ArculusCard, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] +pub struct FactorSourceID { + pub inner: sargon::FactorSourceID, +} +impl FactorSourceID { + pub fn new(inner: impl Borrow) -> Arc { + Arc::new(Self { + inner: *inner.borrow(), + }) + } +} + +#[cfg(test)] +impl FactorSourceID { + pub fn sample_device() -> Arc { + Self::new(sargon::FactorSourceID::sample_device()) + } + + pub fn sample_device_other() -> Arc { + Self::new(sargon::FactorSourceID::sample_device_other()) + } + + pub fn sample_ledger() -> Arc { + Self::new(sargon::FactorSourceID::sample_ledger()) + } + + pub fn sample_ledger_other() -> Arc { + Self::new(sargon::FactorSourceID::sample_ledger_other()) + } + + pub fn sample_arculus() -> Arc { + Self::new(sargon::FactorSourceID::sample_arculus()) + } + + pub fn sample_arculus_other() -> Arc { + Self::new(sargon::FactorSourceID::sample_arculus_other()) + } +} diff --git a/crates/rules/Cargo.toml b/crates/rules/Cargo.toml index 41a1503d..b9ce5098 100644 --- a/crates/rules/Cargo.toml +++ b/crates/rules/Cargo.toml @@ -5,3 +5,13 @@ edition = "2021" [dependencies] thiserror = { workspace = true } +sargon = { workspace = true } +serde = { version = "1.0.215", features = ["derive"] } +pretty_assertions = "1.4.1" +serde_json = { version = "1.0.133", features = ["preserve_order"] } +assert-json-diff = "2.0.2" +once_cell = "1.20.2" +itertools = "0.13.0" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } diff --git a/crates/rules/src/lib.rs b/crates/rules/src/lib.rs index 57fa7b3c..2691d046 100644 --- a/crates/rules/src/lib.rs +++ b/crates/rules/src/lib.rs @@ -1,10 +1,35 @@ -mod rules; +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] + +mod matrices; +mod move_to_sargon; +mod roles; +mod security_structure_of_factors; pub mod prelude { + pub(crate) use sargon::{ + BIP39Passphrase, BaseBaseIsFactorSource, CommonError, DerivationPreset, DisplayName, + FactorInstance, FactorInstances, FactorSource, FactorSourceID, FactorSourceIDFromHash, + FactorSourceKind, FactorSources, HasRoleKindObjectSafe, HasSampleValues, + HierarchicalDeterministicFactorInstance, Identifiable, IndexMap, IndexSet, + IsMaybeKeySpaceAware, IsSecurityStateAware, KeySpace, Mnemonic, MnemonicWithPassphrase, + RoleKind, + }; + + pub(crate) use itertools::*; + + #[cfg(test)] + pub(crate) use sargon::JustKV; - pub(crate) use crate::rules::*; + #[allow(unused_imports)] + pub use crate::matrices::*; + pub use crate::move_to_sargon::*; + pub use crate::roles::*; + pub use crate::security_structure_of_factors::*; - pub(crate) use thiserror::Error as ThisError; + pub(crate) use serde::{Deserialize, Serialize}; + pub(crate) use std::collections::HashSet; + pub(crate) use std::marker::PhantomData; } -pub use prelude::*; +pub use crate::prelude::*; diff --git a/crates/rules/src/matrices/abstract_matrix_builder_or_built.rs b/crates/rules/src/matrices/abstract_matrix_builder_or_built.rs new file mode 100644 index 00000000..16d34267 --- /dev/null +++ b/crates/rules/src/matrices/abstract_matrix_builder_or_built.rs @@ -0,0 +1,44 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbstractMatrixBuilderOrBuilt { + #[serde(skip)] + #[doc(hidden)] + pub(crate) built: PhantomData, + + pub(crate) primary_role: AbstractRoleBuilderOrBuilt<{ ROLE_PRIMARY }, F, U>, + pub(crate) recovery_role: AbstractRoleBuilderOrBuilt<{ ROLE_RECOVERY }, F, U>, + pub(crate) confirmation_role: AbstractRoleBuilderOrBuilt<{ ROLE_CONFIRMATION }, F, U>, + + pub(crate) number_of_days_until_auto_confirm: u16, +} +impl AbstractMatrixBuilderOrBuilt { + pub const DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM: u16 = 14; +} + +pub type AbstractMatrixBuilt = AbstractMatrixBuilderOrBuilt; + +impl AbstractMatrixBuilt { + pub fn primary(&self) -> &AbstractBuiltRoleWithFactor<{ ROLE_PRIMARY }, F> { + &self.primary_role + } + + pub fn recovery(&self) -> &AbstractBuiltRoleWithFactor<{ ROLE_RECOVERY }, F> { + &self.recovery_role + } + + pub fn confirmation(&self) -> &AbstractBuiltRoleWithFactor<{ ROLE_CONFIRMATION }, F> { + &self.confirmation_role + } +} + +impl AbstractMatrixBuilt { + pub fn all_factors(&self) -> HashSet<&F> { + let mut factors = HashSet::new(); + factors.extend(self.primary_role.all_factors()); + factors.extend(self.recovery_role.all_factors()); + factors.extend(self.confirmation_role.all_factors()); + factors + } +} diff --git a/crates/rules/src/matrices/builder/error.rs b/crates/rules/src/matrices/builder/error.rs new file mode 100644 index 00000000..56ad9d8a --- /dev/null +++ b/crates/rules/src/matrices/builder/error.rs @@ -0,0 +1,55 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationViolation { + #[error("Basic violation: {0}")] + Basic(#[from] MatrixRolesInCombinationBasicViolation), + + #[error("Forever invalid: {0}")] + ForeverInvalid(#[from] MatrixRolesInCombinationForeverInvalid), + + #[error("Not yet valid: {0}")] + NotYetValid(#[from] MatrixRolesInCombinationNotYetValid), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationBasicViolation { + #[error("The factor source was not found in any role")] + FactorSourceNotFoundInAnyRole, + + #[error("The number of days until auto confirm must be greater than zero")] + NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationForeverInvalid { + #[error("Recovery and confirmation factors overlap. No factor may be used in both the recovery and confirmation roles")] + RecoveryAndConfirmationFactorsOverlap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationNotYetValid { + #[error("The single factor used in the primary role must not be used in any other role")] + SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixBuilderValidation { + #[error("Role {role:?} in isolation violation: {violation}")] + RoleInIsolation { + role: RoleKind, + violation: RoleBuilderValidation, + }, + #[error("Roles in combination violation: {0}")] + CombinationViolation(#[from] MatrixRolesInCombinationViolation), +} + +pub(crate) trait IntoMatrixErr { + fn into_matrix_err(self, role: RoleKind) -> Result; +} + +impl IntoMatrixErr for Result { + fn into_matrix_err(self, role: RoleKind) -> Result { + self.map_err(|violation| MatrixBuilderValidation::RoleInIsolation { role, violation }) + } +} diff --git a/crates/rules/src/matrices/builder/matrix_builder.rs b/crates/rules/src/matrices/builder/matrix_builder.rs new file mode 100644 index 00000000..5d4b2a2c --- /dev/null +++ b/crates/rules/src/matrices/builder/matrix_builder.rs @@ -0,0 +1,351 @@ +#![allow(clippy::new_without_default)] + +use crate::prelude::*; + +pub type MatrixBuilderMutateResult = Result<(), MatrixBuilderValidation>; +pub type MatrixBuilderBuildResult = Result; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Built; + +pub type MatrixBuilder = AbstractMatrixBuilderOrBuilt< + FactorSourceID, + MatrixOfFactorSourceIds, + Built, // this is HACKY +>; + +// ================== +// ===== PUBLIC ===== +// ================== +impl MatrixBuilder { + pub fn new() -> Self { + Self { + built: PhantomData, + primary_role: PrimaryRoleBuilder::new(), + recovery_role: RecoveryRoleBuilder::new(), + confirmation_role: ConfirmationRoleBuilder::new(), + number_of_days_until_auto_confirm: Self::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, + } + } + + pub fn build(self) -> MatrixBuilderBuildResult { + self.validate_combination()?; + + let primary = self + .primary_role + .build() + .into_matrix_err(RoleKind::Primary)?; + let recovery = self + .recovery_role + .build() + .into_matrix_err(RoleKind::Recovery)?; + let confirmation = self + .confirmation_role + .build() + .into_matrix_err(RoleKind::Confirmation)?; + + let built = MatrixOfFactorSourceIds { + built: PhantomData, + primary_role: primary, + recovery_role: recovery, + confirmation_role: confirmation, + number_of_days_until_auto_confirm: self.number_of_days_until_auto_confirm, + }; + Ok(built) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.primary_role + .validation_for_addition_of_factor_source_of_kind_to_threshold(factor_source_kind) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_primary_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.primary_role + .validation_for_addition_of_factor_source_of_kind_to_override(factor_source_kind) + } + + pub fn validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.primary_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Threshold, + factor_sources, + ) + } + + pub fn validation_for_addition_of_factor_source_to_primary_override_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.primary_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Override, + factor_sources, + ) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_recovery_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.recovery_role + .validation_for_addition_of_factor_source_of_kind_to_override(factor_source_kind) + } + + pub fn validation_for_addition_of_factor_source_to_recovery_override_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.recovery_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Override, + factor_sources, + ) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.confirmation_role + .validation_for_addition_of_factor_source_of_kind_to_override(factor_source_kind) + } + + pub fn validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.confirmation_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Override, + factor_sources, + ) + } + + pub fn validate_each_role_in_isolation(&self) -> MatrixBuilderMutateResult { + self.primary_role + .validate() + .into_matrix_err(RoleKind::Primary)?; + self.recovery_role + .validate() + .into_matrix_err(RoleKind::Recovery)?; + self.confirmation_role + .validate() + .into_matrix_err(RoleKind::Confirmation)?; + Ok(()) + } + + pub fn validate(&self) -> MatrixBuilderMutateResult { + self.validate_each_role_in_isolation()?; + self.validate_combination()?; + Ok(()) + } + + /// Adds the factor source to the primary role threshold list. + pub fn add_factor_source_to_primary_threshold( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.primary_role + .add_factor_source_to_threshold(factor_source_id) + .into_matrix_err(RoleKind::Primary) + } + + /// Adds the factor source to the primary role override list. + pub fn add_factor_source_to_primary_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.primary_role + .add_factor_source_to_override(factor_source_id) + .into_matrix_err(RoleKind::Primary) + } + + pub fn add_factor_source_to_recovery_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.recovery_role + .add_factor_source_to_override(factor_source_id) + .into_matrix_err(RoleKind::Recovery) + } + + pub fn add_factor_source_to_confirmation_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.confirmation_role + .add_factor_source_to_override(factor_source_id) + .into_matrix_err(RoleKind::Confirmation) + } + + pub fn get_confirmation_factors(&self) -> &Vec { + self.confirmation_role.get_override_factors() + } + + pub fn get_recovery_factors(&self) -> &Vec { + self.recovery_role.get_override_factors() + } + + pub fn get_primary_threshold_factors(&self) -> &Vec { + self.primary_role.get_threshold_factors() + } + + pub fn get_primary_override_factors(&self) -> &Vec { + self.primary_role.get_override_factors() + } + + /// Sets the threshold on the primary role builder. + pub fn set_threshold(&mut self, threshold: u8) -> MatrixBuilderMutateResult { + self.primary_role + .set_threshold(threshold) + .into_matrix_err(RoleKind::Primary) + } + + pub fn get_threshold(&self) -> u8 { + self.primary_role.get_threshold() + } + + pub fn set_number_of_days_until_auto_confirm( + &mut self, + number_of_days: u16, + ) -> MatrixBuilderMutateResult { + self.number_of_days_until_auto_confirm = number_of_days; + + self.validate_number_of_days_until_auto_confirm() + } + + pub fn get_number_of_days_until_auto_confirm(&self) -> u16 { + self.number_of_days_until_auto_confirm + } + + /// Removes `factor_source_id` from all three roles, if not found in any an error + /// is thrown. + /// + /// # Throws + /// If none of the three role builders contains the factor source id, `Err(BasicViolation::FactorSourceNotFound)` is thrown + pub fn remove_factor( + &mut self, + factor_source_id: &FactorSourceID, + ) -> MatrixBuilderMutateResult { + let mut found = false; + if self + .primary_role + .remove_factor_source(factor_source_id) + .is_ok() + { + found = true; + } + if self + .recovery_role + .remove_factor_source(factor_source_id) + .is_ok() + { + found = true; + } + if self + .confirmation_role + .remove_factor_source(factor_source_id) + .is_ok() + { + found = true; + } + if !found { + MatrixBuilderMutateResult::Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic( + MatrixRolesInCombinationBasicViolation::FactorSourceNotFoundInAnyRole, + ), + )) + } else { + Ok(()) + } + } +} + +// ================== +// ==== PRIVATE ===== +// ================== +impl MatrixBuilder { + fn validate_if_primary_has_single_it_must_not_be_used_by_any_other_role( + &self, + ) -> MatrixBuilderMutateResult { + let primary_has_single_factor = self.primary_role.all_factors().len() == 1; + if primary_has_single_factor { + let primary_factors = self.primary_role.all_factors(); + let primary_factor = primary_factors.first().unwrap(); + let recovery_set = HashSet::<_>::from_iter(self.recovery_role.get_override_factors()); + let confirmation_set = + HashSet::<_>::from_iter(self.confirmation_role.get_override_factors()); + if recovery_set.contains(primary_factor) || confirmation_set.contains(primary_factor) { + return Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole), + )); + } + } + Ok(()) + } + + fn validate_no_factor_may_be_used_in_both_recovery_and_confirmation( + &self, + ) -> MatrixBuilderMutateResult { + let recovery_set = HashSet::<_>::from_iter(self.recovery_role.get_override_factors()); + let confirmation_set = + HashSet::<_>::from_iter(self.confirmation_role.get_override_factors()); + let intersection = recovery_set + .intersection(&confirmation_set) + .collect::>(); + if intersection.is_empty() { + Ok(()) + } else { + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::ForeverInvalid( + MatrixRolesInCombinationForeverInvalid::RecoveryAndConfirmationFactorsOverlap, + ), + )) + } + } + + fn validate_number_of_days_until_auto_confirm(&self) -> MatrixBuilderMutateResult { + if self.number_of_days_until_auto_confirm == 0 { + return Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic( + MatrixRolesInCombinationBasicViolation::NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero, + ), + )); + } + Ok(()) + } + + /// Security Shield Rules + /// In addition to the factor/role rules above, the wallet must enforce certain rules for combinations of + /// factors across the three roles. The construction method described in the next section will automatically + /// always follow these rules. A user may however choose to manually add/remove factors from their Shield + /// configuration and so the wallet must evaluate these rules and inform the user when the combination they + /// have chosen cannot be used. The wallet should never allow a user to complete a Shield configuration that + /// violates these rules. + /// + /// 1. If only one factor is used for `Primary`, that factor may not be used for either `Recovery` or `Confirmation` + /// 2. No factor may be used (override) in both `Recovery` and `Confirmation` + /// 3. No factor may be used in both the `Primary` threshold and `Primary` override + /// 4. Number of days until auto confirm is greater than zero + fn validate_combination(&self) -> MatrixBuilderMutateResult { + self.validate_if_primary_has_single_it_must_not_be_used_by_any_other_role()?; + self.validate_no_factor_may_be_used_in_both_recovery_and_confirmation()?; + + // N.B. the third 3: + // "3. No factor may be used in both the `Primary` threshold and `Primary` override" + // is already enforced by the RoleBuilder + + self.validate_number_of_days_until_auto_confirm()?; + + Ok(()) + } +} diff --git a/crates/rules/src/matrices/builder/matrix_builder_unit_tests.rs b/crates/rules/src/matrices/builder/matrix_builder_unit_tests.rs new file mode 100644 index 00000000..a5197f3f --- /dev/null +++ b/crates/rules/src/matrices/builder/matrix_builder_unit_tests.rs @@ -0,0 +1,1749 @@ +#![cfg(test)] +use crate::prelude::*; + +#[allow(clippy::upper_case_acronyms)] +type SUT = MatrixBuilder; + +fn make() -> SUT { + SUT::new() +} + +#[test] +fn empty_primary_is_err() { + let sut = make(); + let res = sut.build(); + assert_eq!( + res, + MatrixBuilderBuildResult::Err(MatrixBuilderValidation::RoleInIsolation { + role: RoleKind::Primary, + violation: RoleBuilderValidation::NotYetValid( + NotYetValidReason::RoleMustHaveAtLeastOneFactor + ) + }) + ) +} + +#[test] +fn empty_recovery_is_err() { + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + let res = sut.build(); + assert_eq!( + res, + MatrixBuilderBuildResult::Err(MatrixBuilderValidation::RoleInIsolation { + role: RoleKind::Recovery, + violation: RoleBuilderValidation::NotYetValid( + NotYetValidReason::RoleMustHaveAtLeastOneFactor + ) + }) + ) +} + +#[test] +fn empty_confirmation_is_err() { + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_arculus()) + .unwrap(); + let res = sut.build(); + assert_eq!( + res, + MatrixBuilderBuildResult::Err(MatrixBuilderValidation::RoleInIsolation { + role: RoleKind::Confirmation, + violation: RoleBuilderValidation::NotYetValid( + NotYetValidReason::RoleMustHaveAtLeastOneFactor + ) + }) + ) +} + +#[test] +fn set_number_of_days_cannot_be_zero() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + sut.number_of_days_until_auto_confirm = 0; // bypass validation + + // Build + let validation = MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic(MatrixRolesInCombinationBasicViolation::NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero) + ); + assert_eq!(sut.validate(), Err(validation)); + let res = sut.build(); + assert_eq!(res, Err(validation)); +} + +#[test] +fn set_number_of_days_42() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + sut.set_number_of_days_until_auto_confirm(42).unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles_and_days( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_device(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([FactorSourceID::sample_ledger(),],), + RoleWithFactorSourceIds::confirmation_with_factors([FactorSourceID::sample_password()],), + 42, + ) + ); +} + +#[test] +fn auto_confirm_default() { + assert_eq!(SUT::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, 14); +} + +#[test] +fn set_number_of_days_if_not_set_uses_default() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles_and_days( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_device(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([FactorSourceID::sample_ledger(),],), + RoleWithFactorSourceIds::confirmation_with_factors([FactorSourceID::sample_password()],), + SUT::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, + ) + ); +} + +#[test] +fn sample_factor_cannot_be_both_in_threshold_and_override() { + let mut sut = make(); + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(fs).unwrap(); + let res = sut.add_factor_source_to_primary_override(fs); + assert!(res.is_err()); +} + +#[test] +fn single_factor_in_primary_threshold_cannot_be_in_recovery() { + let mut sut = make(); + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_threshold(fs).unwrap(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_arculus_other()) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // ACT + sut.add_factor_source_to_recovery_override(fs).unwrap(); + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); + + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_arculus()) + .unwrap(); + + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built.primary(), + &RoleWithFactorSourceIds::primary_with_factors( + 1, + [ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ], + [] + ) + ); + pretty_assertions::assert_eq!( + built.recovery(), + &RoleWithFactorSourceIds::recovery_with_factors([FactorSourceID::sample_ledger()]), + ); + + pretty_assertions::assert_eq!( + built.confirmation(), + &RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_arculus_other() + ]) + ) +} + +#[test] +fn single_factor_in_primary_override_cannot_be_in_recovery() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(fs).unwrap(); + sut.add_factor_source_to_recovery_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +#[test] +fn single_factor_in_primary_threshold_cannot_be_in_confirmation() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_arculus()) + .unwrap(); + _ = sut.set_threshold(1); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_threshold(fs).unwrap(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +#[test] +fn single_factor_in_primary_override_cannot_be_in_confirmation() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(fs).unwrap(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +#[test] +fn add_factor_to_recovery_then_same_to_confirmation_is_err() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + sut.add_factor_source_to_recovery_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!( + res, + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::ForeverInvalid( + MatrixRolesInCombinationForeverInvalid::RecoveryAndConfirmationFactorsOverlap + ) + )) + ); +} + +#[test] +fn add_factor_to_confirmation_then_same_to_override_when_validated_is_err() { + // ARRANGE + let mut sut = make(); + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ACT + sut.add_factor_source_to_recovery_override(fs).unwrap(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!( + res, + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::ForeverInvalid( + MatrixRolesInCombinationForeverInvalid::RecoveryAndConfirmationFactorsOverlap + ) + )) + ); +} + +#[test] +fn add_factor_to_confirmation_then_same_to_primary_threshold_is_not_yet_valid() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_arculus()) + .unwrap(); + _ = sut.set_threshold(1); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_recovery_override(fs).unwrap(); + sut.add_factor_source_to_primary_threshold(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +mod remove { + use super::*; + + #[test] + fn not_found() { + let mut sut = make(); + let res = sut.remove_factor(&FactorSourceID::sample_device()); + assert_eq!( + res, + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic( + MatrixRolesInCombinationBasicViolation::FactorSourceNotFoundInAnyRole + ) + )) + ); + } + + #[test] + fn remove_from_primary_threshold_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.remove_factor(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } + + #[test] + fn remove_from_primary_override_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.remove_factor(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } + + #[test] + fn remove_from_recovery_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.remove_factor(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } + + #[test] + fn remove_from_confirmation_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.remove_factor(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } +} + +mod validation_for_addition_of_factor_source_for_each { + use super::*; + + mod primary { + + use super::*; + + #[test] + fn empty() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::new(), + ); + assert_eq!(xs, IndexSet::new()); + } + + #[test] + fn device_threshold_3x_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + + #[test] + fn device_2x_threshold_override_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + + #[test] + fn device_threshold_override_2x_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + + #[test] + fn device_2x_override_threshold_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + sargon::RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + } + + mod recovery { + use super::*; + + fn role() -> sargon::RoleKind { + sargon::RoleKind::Recovery + } + + #[test] + fn empty() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + &IndexSet::new(), + ); + assert_eq!(xs, IndexSet::new()); + } + + #[test] + fn supported() { + let sut = make(); + let fsids = vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + FactorSourceID::sample_arculus(), + FactorSourceID::sample_arculus_other(), + FactorSourceID::sample_passphrase(), + FactorSourceID::sample_passphrase_other(), + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ]; + let xs = sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + &IndexSet::from_iter(fsids.clone()), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + fsids + .into_iter() + .map(|fsid| FactorSourceInRoleBuilderValidationStatus::ok(role(), fsid)) + .collect::>() + ); + } + + #[test] + fn password_and_security_questions_not_supported() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + ]), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + [ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + ] + .into_iter() + .map( + |fsid| FactorSourceInRoleBuilderValidationStatus::forever_invalid( + role(), + fsid, + if fsid.get_factor_source_kind() == FactorSourceKind::SecurityQuestions { + ForeverInvalidReason::RecoveryRoleSecurityQuestionsNotSupported + } else { + ForeverInvalidReason::RecoveryRolePasswordNotSupported + } + ) + ) + .collect::>() + ); + } + } + + mod confirmation { + use super::*; + + fn role() -> sargon::RoleKind { + sargon::RoleKind::Confirmation + } + + #[test] + fn empty() { + let sut = make(); + let xs = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &IndexSet::new(), + ); + assert_eq!(xs, IndexSet::new()); + } + + #[test] + fn supported() { + let sut = make(); + let fsids = vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + FactorSourceID::sample_arculus(), + FactorSourceID::sample_arculus_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_passphrase(), + FactorSourceID::sample_passphrase_other(), + ]; + let xs = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &IndexSet::from_iter(fsids.clone()), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + fsids + .into_iter() + .map(|fsid| FactorSourceInRoleBuilderValidationStatus::ok(role(), fsid)) + .collect::>() + ); + } + + #[test] + fn password_and_security_questions_not_supported() { + let sut = make(); + let xs = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ]), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + [ + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ] + .into_iter() + .map( + |fsid| FactorSourceInRoleBuilderValidationStatus::forever_invalid( + role(), + fsid, + ForeverInvalidReason::ConfirmationRoleTrustedContactNotSupported + ) + ) + .collect::>() + ); + } + } +} + +mod validation_of_addition_of_kind { + use super::*; + + mod recovery { + use super::*; + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_recovery_override_empty() { + let sut = make(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_recovery_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, false); + test(FactorSourceKind::Password, false); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, true); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_recovery_override_single_recovery() { + let mut sut = make(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_recovery_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, false); + test(FactorSourceKind::Password, false); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, true); + } + } + + mod confirmation { + use super::*; + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_confirmation_override_empty() { + let sut = make(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_confirmation_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, true); + test(FactorSourceKind::Password, true); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, false); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_confirmation_override_single_recovery( + ) { + let mut sut = make(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_ledger()) + .unwrap(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_confirmation_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, true); + test(FactorSourceKind::Password, true); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, false); + } + } + + mod primary { + use super::*; + + #[test] + fn ledger_threshold_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn ledger_threshold_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn ledger_override_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn ledger_override_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_threshold_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_arculus()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_threshold_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_arculus()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_override_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_override_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn security_questions_not_supported_threshold() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::SecurityQuestions, + ); + assert!(res.is_err()); + } + + #[test] + fn security_questions_not_supported_override() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::SecurityQuestions, + ); + assert!(res.is_err()); + } + + #[test] + fn trusted_contact_not_supported_threshold() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::TrustedContact, + ); + assert!(res.is_err()); + } + + #[test] + fn trusted_contact_not_supported_override() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::TrustedContact, + ); + assert!(res.is_err()); + } + + #[test] + fn passphrase_threshold_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_passphrase()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn passphrase_threshold_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_passphrase()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn passphrase_override_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_passphrase()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn passphrase_override_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_passphrase()) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn thresehold_password_alone_is_err() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Password, + ); + assert!(res.is_err()); + } + + #[test] + fn thresehold_password_not_alone() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_arculus()) + .unwrap(); + _ = sut.set_threshold(2); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Password, + ); + assert!(res.is_ok()); + } + + #[test] + fn device_is_err_for_second_3x_threshold() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_2x_threshold_override() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_threshold_override_threshold() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_threshold_override_2x() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_3x_override() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_2x_override_threshold() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_override_threshold_2x() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_override_threshold_override() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + } +} + +mod shield_configs { + use super::*; + + mod mvp { + + use super::*; + + #[test] + fn config_1_1() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + assert_eq!(built, MatrixOfFactorSourceIds::sample_config_11()); + } + + #[test] + fn config_1_2() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + let res = sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_password()); + + assert_eq!( + res, + Err(MatrixBuilderValidation::RoleInIsolation { role: RoleKind::Primary, violation: RoleBuilderValidation::NotYetValid(NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne)} + )); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_password() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + } + + #[test] + fn config_1_3() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let res = sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_password()); + + assert_eq!( + res, + Err(MatrixBuilderValidation::RoleInIsolation { role: RoleKind::Primary, violation: RoleBuilderValidation::NotYetValid(NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne)} + )); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_password() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_13()) + } + + #[test] + fn config_1_4() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_device(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger() + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_14()) + } + + #[test] + fn config_1_5() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_ledger(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device() + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_15()) + } + + #[test] + fn config_2_1() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_21()) + } + + #[test] + fn config_2_2() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger_other()) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_22()) + } + + #[test] + fn config_2_3() { + let mut sut = make(); + + // Primary + // TODO: Ask Matt about this, does he mean Threshold(1) or Override? + sut.add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 0, + [], + [FactorSourceID::sample_ledger(),], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_23()) + } + + #[test] + fn config_2_4() { + let mut sut = make(); + + // Primary + // TODO: Ask Matt about this, does he mean Threshold(1) or Override? + sut.add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 0, + [], + [FactorSourceID::sample_device(),], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_ledger_other() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_24()) + } + + #[test] + fn config_3() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_password() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_30()) + } + + #[test] + fn config_4() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_password_other()) + .unwrap(); + sut.add_factor_source_to_confirmation_override(FactorSourceID::sample_passphrase()) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_passphrase() + ],), + ) + ); + + pretty_assertions::assert_eq!(built, MatrixOfFactorSourceIds::sample_config_40()) + } + } +} diff --git a/crates/rules/src/matrices/builder/mod.rs b/crates/rules/src/matrices/builder/mod.rs new file mode 100644 index 00000000..139df14b --- /dev/null +++ b/crates/rules/src/matrices/builder/mod.rs @@ -0,0 +1,7 @@ +mod error; +mod matrix_builder; +mod matrix_builder_unit_tests; + +pub use error::*; +#[allow(unused_imports)] +pub use matrix_builder::*; diff --git a/crates/rules/src/matrices/matrix_of_factor_instances.rs b/crates/rules/src/matrices/matrix_of_factor_instances.rs new file mode 100644 index 00000000..54b505a0 --- /dev/null +++ b/crates/rules/src/matrices/matrix_of_factor_instances.rs @@ -0,0 +1,311 @@ +use crate::prelude::*; + +pub type MatrixOfFactorInstances = AbstractMatrixBuilderOrBuilt; + +impl HasFactorInstances for MatrixOfFactorInstances { + fn unique_factor_instances(&self) -> IndexSet { + let mut set = IndexSet::new(); + set.extend(self.primary_role.all_factors().into_iter().cloned()); + set.extend(self.recovery_role.all_factors().into_iter().cloned()); + set.extend(self.confirmation_role.all_factors().into_iter().cloned()); + set + } +} + +impl MatrixOfFactorInstances { + fn from_matrix_of_sources(matrix_of_sources: MatrixOfFactorSources) -> Self { + let mut consuming_instances = MnemonicWithPassphrase::derive_instances_for_factor_sources( + sargon::NetworkID::Mainnet, + 1, + [DerivationPreset::AccountMfa], + matrix_of_sources.all_factors().into_iter().cloned(), + ); + + Self::fulfilling_matrix_of_factor_sources_with_instances( + &mut consuming_instances, + matrix_of_sources.clone(), + ) + .unwrap() + } +} + +impl HasSampleValues for MatrixOfFactorInstances { + fn sample() -> Self { + Self::from_matrix_of_sources(MatrixOfFactorSources::sample()) + } + + fn sample_other() -> Self { + Self::from_matrix_of_sources(MatrixOfFactorSources::sample_other()) + } +} + +impl MatrixOfFactorInstances { + /// Maps `MatrixOfFactorSources -> MatrixOfFactorInstances` by + /// "assigning" FactorInstances to each MatrixOfFactorInstances from + /// `consuming_instances`. + /// + /// NOTE: + /// **One FactorInstance might be used multiple times in the MatrixOfFactorInstances, + /// e.g. ones in the PrimaryRole(WithFactorInstances) and again in RecoveryRole(WithFactorInstances) or + /// in RecoveryRole(WithFactorInstances)**. + /// + /// However, the same FactorInstance is NEVER used in two different MatrixOfFactorInstances. + /// + /// + pub fn fulfilling_matrix_of_factor_sources_with_instances( + consuming_instances: &mut IndexMap, + matrix_of_factor_sources: MatrixOfFactorSources, + ) -> Result { + let instances = &consuming_instances.clone(); + + let primary_role = + PrimaryRoleWithFactorInstances::fulfilling_role_of_factor_sources_with_factor_instances( + instances, + &matrix_of_factor_sources, + )?; + let recovery_role = + RecoveryRoleWithFactorInstances::fulfilling_role_of_factor_sources_with_factor_instances( + instances, + &matrix_of_factor_sources, + )?; + let confirmation_role = + ConfirmationRoleWithFactorInstances::fulfilling_role_of_factor_sources_with_factor_instances( + instances, + &matrix_of_factor_sources, + )?; + + let matrix = Self { + built: PhantomData, + primary_role, + recovery_role, + confirmation_role, + number_of_days_until_auto_confirm: matrix_of_factor_sources + .number_of_days_until_auto_confirm, + }; + + // Now that we have assigned instances, **possibly the SAME INSTANCE to multiple roles**, + // lets delete them from the `consuming_instances` map. + for instance in matrix.all_factors() { + let fsid = &FactorSourceIDFromHash::try_from(instance.factor_source_id).unwrap(); + let existing = consuming_instances.get_mut(fsid).unwrap(); + + let to_remove = + HierarchicalDeterministicFactorInstance::try_from(instance.clone()).unwrap(); + + // We remove at the beginning of the list first. + existing.shift_remove(&to_remove); + + if existing.is_empty() { + // not needed per se, but feels prudent to "prune". + consuming_instances.shift_remove_entry(fsid); + } + } + + Ok(matrix) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = MatrixOfFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + assert_ne!( + SUT::sample().unique_factor_instances(), + SUT::sample_other().unique_factor_instances() + ); + } + + #[test] + fn err_if_no_instance_found_for_factor_source() { + assert!(matches!( + SUT::fulfilling_matrix_of_factor_sources_with_instances( + &mut IndexMap::new(), + MatrixOfFactorSources::sample() + ), + Err(CommonError::MissingFactorMappingInstancesIntoRole) + )); + } + + #[test] + fn err_if_empty_instance_found_for_factor_source() { + assert!(matches!( + SUT::fulfilling_matrix_of_factor_sources_with_instances( + &mut IndexMap::kv( + FactorSource::sample_device_babylon().id_from_hash(), + FactorInstances::from_iter([]) + ), + MatrixOfFactorSources::sample() + ), + Err(CommonError::MissingFactorMappingInstancesIntoRole) + )); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "primaryRole": { + "threshold": 2, + "thresholdFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "427969814e15d74c3ff4d9971465cb709d210c8a7627af9466bdaa67bd0929b7" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + }, + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "427969814e15d74c3ff4d9971465cb709d210c8a7627af9466bdaa67bd0929b7" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + }, + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "4af49eb56b1af579aaf03f1760ec526f56e2297651f7a067f4b362f685417a81" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + "#, + ); + } +} diff --git a/crates/rules/src/matrices/matrix_of_factor_source_ids.rs b/crates/rules/src/matrices/matrix_of_factor_source_ids.rs new file mode 100644 index 00000000..cedac836 --- /dev/null +++ b/crates/rules/src/matrices/matrix_of_factor_source_ids.rs @@ -0,0 +1,732 @@ +use crate::prelude::*; + +pub type MatrixOfFactorSourceIds = AbstractMatrixBuilt; + +#[cfg(test)] +impl MatrixOfFactorSourceIds { + pub(crate) fn with_roles_and_days( + primary: PrimaryRoleWithFactorSourceIds, + recovery: RecoveryRoleWithFactorSourceIds, + confirmation: ConfirmationRoleWithFactorSourceIds, + number_of_days_until_auto_confirm: u16, + ) -> Self { + assert_eq!(primary.role(), sargon::RoleKind::Primary); + assert_eq!(recovery.role(), sargon::RoleKind::Recovery); + assert_eq!(confirmation.role(), sargon::RoleKind::Confirmation); + Self { + built: PhantomData, + primary_role: primary, + recovery_role: recovery, + confirmation_role: confirmation, + number_of_days_until_auto_confirm, + } + } + + pub(crate) fn with_roles( + primary: PrimaryRoleWithFactorSourceIds, + recovery: RecoveryRoleWithFactorSourceIds, + confirmation: ConfirmationRoleWithFactorSourceIds, + ) -> Self { + Self::with_roles_and_days( + primary, + recovery, + confirmation, + Self::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, + ) + } +} + +impl MatrixOfFactorSourceIds { + pub fn sample_config_11() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(2).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_12() -> Self { + let mut builder = MatrixBuilder::new(); + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + _ = builder.add_factor_source_to_primary_threshold(FactorSourceID::sample_password()); + + _ = builder.set_threshold(2); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_13() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let res = builder.add_factor_source_to_primary_threshold(FactorSourceID::sample_password()); + + assert_eq!( + res, + Err(MatrixBuilderValidation::RoleInIsolation { role: RoleKind::Primary, violation: RoleBuilderValidation::NotYetValid(NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne)} + )); + builder.set_threshold(2).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_14() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + builder.set_threshold(1).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_15() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(1).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_21() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(2).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_22() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger_other()) + .unwrap(); + builder.set_threshold(2).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_23() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + // TODO: Ask Matt about this, does he mean Threshold(1) or Override? + builder + .add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_24() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + // TODO: Ask Matt about this, does he mean Threshold(1) or Override? + builder + .add_factor_source_to_primary_override(FactorSourceID::sample_device()) + .unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Build + builder.build().unwrap() + } + + pub fn sample_config_30() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(2).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger_other()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_40() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(2).unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password_other()) + .unwrap(); + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_passphrase()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_51() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let _ = builder.set_threshold(2); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_password()) + .unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_trusted_contact()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_52() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let _ = builder.set_threshold(2); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_password()) + .unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_trusted_contact()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_trusted_contact_other()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password()) + .unwrap(); + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_password_other()) + .unwrap(); + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_passphrase()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_60() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let _ = builder.set_threshold(1); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_trusted_contact()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_security_questions()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_70() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let _ = builder.set_threshold(2); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_trusted_contact()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_device()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_80() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let _ = builder.set_threshold(2); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_security_questions()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } + + pub fn sample_config_90() -> Self { + let mut builder = MatrixBuilder::new(); + + // Primary + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + let _ = builder.set_threshold(2); + builder + .add_factor_source_to_primary_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + + // Recovery + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_trusted_contact()) + .unwrap(); + builder + .add_factor_source_to_recovery_override(FactorSourceID::sample_device()) + .unwrap(); + + // Confirmation + builder + .add_factor_source_to_confirmation_override(FactorSourceID::sample_security_questions()) + .unwrap(); + + // Build + assert!(builder.validate().is_ok()); + builder.build().unwrap() + } +} + +impl HasSampleValues for MatrixOfFactorSourceIds { + fn sample() -> Self { + Self::sample_config_11() + } + + fn sample_other() -> Self { + Self::sample_config_24() + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[allow(clippy::upper_case_acronyms)] + type SUT = MatrixOfFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + assert_ne!(SUT::sample(), SUT::sample_config_12()); + assert_ne!(SUT::sample().primary(), SUT::sample_other().primary()); + assert_ne!(SUT::sample().recovery(), SUT::sample_other().recovery()); + assert_ne!( + SUT::sample().confirmation(), + SUT::sample_other().confirmation() + ); + } + + #[test] + fn hash() { + assert_eq!( + HashSet::::from_iter([ + SUT::sample_config_11(), + SUT::sample_config_12(), + SUT::sample_config_13(), + SUT::sample_config_14(), + SUT::sample_config_15(), + SUT::sample_config_21(), + SUT::sample_config_22(), + SUT::sample_config_23(), + SUT::sample_config_24(), + SUT::sample_config_30(), + SUT::sample_config_40(), + SUT::sample_config_51(), + SUT::sample_config_52(), + SUT::sample_config_60(), + SUT::sample_config_70(), + SUT::sample_config_80(), + SUT::sample_config_90(), + // Duplicates should be removed + SUT::sample_config_11(), + SUT::sample_config_12(), + SUT::sample_config_13(), + SUT::sample_config_14(), + SUT::sample_config_15(), + SUT::sample_config_21(), + SUT::sample_config_22(), + SUT::sample_config_23(), + SUT::sample_config_24(), + SUT::sample_config_30(), + SUT::sample_config_40(), + SUT::sample_config_51(), + SUT::sample_config_52(), + SUT::sample_config_60(), + SUT::sample_config_70(), + SUT::sample_config_80(), + SUT::sample_config_90(), + ]) + .len(), + 17 + ); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "primaryRole": { + "threshold": 2, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + "#, + ); + } + + #[test] + fn assert_json_sample_other() { + let sut = SUT::sample_other(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "primaryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + } + ] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "52ef052a0642a94279b296d6b3b17dedc035a7ae37b76c1d60f11f2725100077" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + "#, + ); + } +} diff --git a/crates/rules/src/matrices/matrix_of_factor_sources.rs b/crates/rules/src/matrices/matrix_of_factor_sources.rs new file mode 100644 index 00000000..b083100f --- /dev/null +++ b/crates/rules/src/matrices/matrix_of_factor_sources.rs @@ -0,0 +1,65 @@ +use crate::prelude::*; + +pub type MatrixOfFactorSources = AbstractMatrixBuilt; + +impl MatrixOfFactorSources { + pub fn new( + matrix: MatrixOfFactorSourceIds, + factor_sources: &FactorSources, + ) -> Result { + let primary_role = RoleWithFactorSources::new(matrix.primary_role, factor_sources)?; + + let recovery_role = RoleWithFactorSources::new(matrix.recovery_role, factor_sources)?; + + let confirmation_role = + RoleWithFactorSources::new(matrix.confirmation_role, factor_sources)?; + + if primary_role.role() != RoleKind::Primary + || recovery_role.role() != RoleKind::Recovery + || confirmation_role.role() != RoleKind::Confirmation + { + unreachable!("Programmer error!") + } + + Ok(Self { + built: PhantomData, + primary_role, + recovery_role, + confirmation_role, + number_of_days_until_auto_confirm: matrix.number_of_days_until_auto_confirm, + }) + } +} + +impl HasSampleValues for MatrixOfFactorSources { + fn sample() -> Self { + let ids = MatrixOfFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } + + fn sample_other() -> Self { + let ids = MatrixOfFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = MatrixOfFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/rules/src/matrices/mod.rs b/crates/rules/src/matrices/mod.rs new file mode 100644 index 00000000..4c9d36a2 --- /dev/null +++ b/crates/rules/src/matrices/mod.rs @@ -0,0 +1,12 @@ +mod abstract_matrix_builder_or_built; +mod builder; +mod matrix_of_factor_instances; +mod matrix_of_factor_source_ids; +mod matrix_of_factor_sources; + +pub(crate) use abstract_matrix_builder_or_built::*; +#[allow(unused_imports)] +pub use builder::*; +pub use matrix_of_factor_instances::*; +pub use matrix_of_factor_source_ids::*; +pub use matrix_of_factor_sources::*; diff --git a/crates/rules/src/move_to_sargon.rs b/crates/rules/src/move_to_sargon.rs new file mode 100644 index 00000000..e413f3d2 --- /dev/null +++ b/crates/rules/src/move_to_sargon.rs @@ -0,0 +1,527 @@ +use crate::prelude::*; + +/// A kind of factor list, either threshold, or override kind. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum FactorListKind { + Threshold, + Override, +} + +pub trait HasFactorInstances { + fn unique_factor_instances(&self) -> IndexSet; +} + +/// TODO move to Sargon!!!! +pub trait HasFactorSourceKindObjectSafe { + fn get_factor_source_kind(&self) -> FactorSourceKind; +} +impl HasFactorSourceKindObjectSafe for FactorSourceID { + fn get_factor_source_kind(&self) -> FactorSourceKind { + match self { + FactorSourceID::Hash { value } => value.kind, + FactorSourceID::Address { value } => value.kind, + } + } +} + +#[allow(dead_code)] +// TODO REMOVE once migrated to sargon +pub trait SampleValues: Sized { + fn sample_device() -> Self; + fn sample_device_other() -> Self; + fn sample_ledger() -> Self; + fn sample_ledger_other() -> Self; + fn sample_arculus() -> Self; + fn sample_arculus_other() -> Self; + fn sample_password() -> Self; + fn sample_password_other() -> Self; + fn sample_passphrase() -> Self; + fn sample_passphrase_other() -> Self; + fn sample_security_questions() -> Self; + + fn sample_security_questions_other() -> Self; + fn sample_trusted_contact() -> Self; + fn sample_trusted_contact_other() -> Self; +} + +impl SampleValues for FactorSourceID { + fn sample_device() -> Self { + FactorSourceIDFromHash::sample_device().into() + } + fn sample_ledger() -> Self { + FactorSourceIDFromHash::sample_ledger().into() + } + fn sample_ledger_other() -> Self { + FactorSourceIDFromHash::sample_ledger_other().into() + } + fn sample_arculus() -> Self { + FactorSourceIDFromHash::sample_arculus().into() + } + fn sample_arculus_other() -> Self { + FactorSourceIDFromHash::sample_arculus_other().into() + } + + fn sample_password() -> Self { + FactorSourceIDFromHash::sample_password().into() + } + + fn sample_password_other() -> Self { + FactorSourceIDFromHash::sample_password_other().into() + } + + /// Matt calls `off_device_mnemonic` "passphrase" + fn sample_passphrase() -> Self { + FactorSourceIDFromHash::sample_off_device().into() + } + /// Matt calls `off_device_mnemonic` "passphrase" + fn sample_passphrase_other() -> Self { + FactorSourceIDFromHash::sample_off_device_other().into() + } + fn sample_security_questions() -> Self { + FactorSourceIDFromHash::sample_security_questions().into() + } + fn sample_device_other() -> Self { + FactorSourceIDFromHash::sample_device_other().into() + } + fn sample_security_questions_other() -> Self { + FactorSourceIDFromHash::sample_security_questions_other().into() + } + fn sample_trusted_contact() -> Self { + sargon::FactorSource::sample_trusted_contact_frank().id() + } + fn sample_trusted_contact_other() -> Self { + sargon::FactorSource::sample_trusted_contact_grace().id() + } +} + +use assert_json_diff::assert_json_include; +use core::fmt::Debug; +use pretty_assertions::assert_eq; +use sargon::{DerivationPath, FactorInstancesCache, NetworkID, NextDerivationEntityIndexAssigner}; +use serde::de::DeserializeOwned; +use serde_json::Value; +use std::str::FromStr; +use thiserror::Error as ThisError; + +#[derive(Debug, ThisError)] +pub enum TestingError { + #[error("File contents is not valid JSON '{0}'")] + FailedDoesNotContainValidJSON(String), + + #[error("Failed to JSON deserialize string")] + FailedToDeserialize(serde_json::Error), +} + +/// `name` is file name without extension, assuming it is json file + +pub fn fixture_and_json<'a, T>(vector: &str) -> Result<(T, serde_json::Value), TestingError> +where + T: for<'de> Deserialize<'de>, +{ + let json = serde_json::Value::from_str(vector) + .map_err(|_| TestingError::FailedDoesNotContainValidJSON(vector.to_owned()))?; + + serde_json::from_value::(json.clone()) + .map_err(TestingError::FailedToDeserialize) + .map(|v| (v, json)) +} + +/// `name` is file name without extension, assuming it is json file + +#[allow(unused)] +pub fn fixture<'a, T>(vector: &str) -> Result +where + T: for<'de> Deserialize<'de>, +{ + fixture_and_json(vector).map(|t| t.0) +} + +fn base_assert_equality_after_json_roundtrip(model: &T, json: Value, expect_eq: bool) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let serialized = serde_json::to_value(model).unwrap(); + let deserialized: T = serde_json::from_value(json.clone()).unwrap(); + if expect_eq { + pretty_assertions::assert_eq!(&deserialized, model, "Expected `model: T` and `T` deserialized from `json_string`, to be equal, but they were not."); + assert_json_include!(actual: serialized, expected: json); + } else { + pretty_assertions::assert_ne!(model, &deserialized); + pretty_assertions::assert_ne!(&deserialized, model, "Expected difference between `model: T` and `T` deserialized from `json_string`, but they were unexpectedly equal."); + pretty_assertions::assert_ne!(serialized, json, "Expected difference between `json` (string) and json serialized from `model`, but they were unexpectedly equal."); + } +} + +/// Asserts that (pseudocode) `model.to_json() == json_string` (serialization) +/// and also asserts the associative property: +/// `Model::from_json(json_string) == model` (deserialization) +pub fn assert_eq_after_json_roundtrip(model: &T, json_string: &str) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let json = json_string.parse::().unwrap(); + base_assert_equality_after_json_roundtrip(model, json, true) +} + +pub fn print_json(model: &T) +where + T: Serialize, +{ + println!( + "{}", + serde_json::to_string_pretty(model) + .expect("Should be able to JSON serialize passed in serializable model.") + ); +} + +/// Asserts that (pseudocode) `model.to_json() == json` (serialization) +/// and also asserts the associative property: +/// `Model::from_json(json) == model` (deserialization) + +pub fn assert_json_value_eq_after_roundtrip(model: &T, json: Value) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + base_assert_equality_after_json_roundtrip(model, json, true) +} + +/// Asserts that (pseudocode) `model.to_json() != json_string` (serialization) +/// and also asserts the associative property: +/// `Model::from_json(json_string) != model` (deserialization) + +pub fn assert_ne_after_json_roundtrip(model: &T, json_string: &str) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let json = json_string.parse::().unwrap(); + base_assert_equality_after_json_roundtrip(model, json, false) +} + +/// Asserts that (pseudocode) `model.to_json() != json` (serialization) +/// and also asserts the associative property: +/// `Model::from_json(json) != model` (deserialization) + +pub fn assert_json_value_ne_after_roundtrip(model: &T, json: Value) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + base_assert_equality_after_json_roundtrip(model, json, false) +} + +/// Asserts that (pseudocode) `Model::from_json(model.to_json()) == model`, +/// i.e. that a model after JSON roundtripping remain unchanged. + +pub fn assert_json_roundtrip(model: &T) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let serialized = serde_json::to_value(model).unwrap(); + let deserialized: T = serde_json::from_value(serialized.clone()).unwrap(); + assert_eq!(model, &deserialized); +} + +/// Creates JSON from `json_str` and tries to decode it, then encode the decoded, +/// value and compare it to the JSON value of the json_str. + +pub fn assert_json_str_roundtrip(json_str: &str) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let value = serde_json::Value::from_str(json_str).unwrap(); + let deserialized: T = serde_json::from_value(value.clone()).unwrap(); + let serialized = serde_json::to_value(&deserialized).unwrap(); + assert_eq!(value, serialized); +} + +pub fn assert_json_value_fails(json: Value) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let result = serde_json::from_value::(json.clone()); + + if let Ok(t) = result { + panic!( + "Expected JSON serialization to fail, but it did not, deserialized into: {:?},\n\nFrom JSON: {}", + t, + serde_json::to_string(&json).unwrap() + ); + } + // all good, expected fail. +} + +pub fn assert_json_fails(json_string: &str) +where + T: Serialize + DeserializeOwned + PartialEq + Debug, +{ + let json = json_string.parse::().unwrap(); + assert_json_value_fails::(json) +} + +pub fn assert_json_eq_ignore_whitespace(json1: &str, json2: &str) { + let value1: Value = serde_json::from_str(json1).expect("Invalid JSON in json1"); + let value2: Value = serde_json::from_str(json2).expect("Invalid JSON in json2"); + assert_eq!(value1, value2, "JSON strings do not match"); +} + +pub trait MnemonicWithPassphraseSamples: Sized { + fn sample_device() -> Self; + + fn sample_device_other() -> Self; + + fn sample_device_12_words() -> Self; + + fn sample_device_12_words_other() -> Self; + + fn sample_ledger() -> Self; + + fn sample_ledger_other() -> Self; + + fn sample_off_device() -> Self; + + fn sample_off_device_other() -> Self; + + fn sample_arculus() -> Self; + + fn sample_arculus_other() -> Self; + + fn sample_security_questions() -> Self; + + fn sample_security_questions_other() -> Self; + + fn sample_password() -> Self; + + fn sample_password_other() -> Self; + + fn all_samples() -> Vec { + vec![ + Self::sample_device(), + Self::sample_device_other(), + Self::sample_device_12_words(), + Self::sample_device_12_words_other(), + Self::sample_ledger(), + Self::sample_ledger_other(), + Self::sample_off_device(), + Self::sample_off_device_other(), + Self::sample_arculus(), + Self::sample_arculus_other(), + Self::sample_security_questions(), + Self::sample_security_questions_other(), + Self::sample_password(), + Self::sample_password_other(), + ] + } + + fn derive_instances_for_factor_sources( + network_id: NetworkID, + quantity_per_factor: usize, + derivation_presets: impl IntoIterator, + sources: impl IntoIterator, + ) -> IndexMap { + let next_index_assigner = NextDerivationEntityIndexAssigner::new( + network_id, + None, + FactorInstancesCache::default(), + ); + + let derivation_presets = derivation_presets.into_iter().collect::>(); + + sources + .into_iter() + .map(|fs| { + let fsid = fs.id_from_hash(); + let mwp = fsid.sample_associated_mnemonic(); + + let paths = derivation_presets + .clone() + .into_iter() + .map(|dp| (dp, quantity_per_factor)) + .collect::>(); + + let paths = paths + .into_iter() + .flat_map(|(derivation_preset, qty)| { + // `qty` many paths + (0..qty) + .map(|_| { + let index_agnostic_path = + derivation_preset.index_agnostic_path_on_network(network_id); + + next_index_assigner + .next(fsid, index_agnostic_path) + .map(|index| DerivationPath::from((index_agnostic_path, index))) + .unwrap() + }) + .collect::>() + }) + .collect::>(); + + let instances = mwp + .derive_public_keys(paths) + .into_iter() + .map(|public_key| { + HierarchicalDeterministicFactorInstance::new(fsid, public_key) + }) + .collect::(); + + (fsid, instances) + }) + .collect::>() + } +} + +use once_cell::sync::Lazy; + +pub(crate) static MNEMONIC_BY_ID_MAP: Lazy< + IndexMap, +> = Lazy::new(|| { + IndexMap::from_iter([ + ( + FactorSourceIDFromHash::sample_device(), + MnemonicWithPassphrase::sample_device(), + ), + ( + FactorSourceIDFromHash::sample_ledger(), + MnemonicWithPassphrase::sample_ledger(), + ), + ( + FactorSourceIDFromHash::sample_ledger_other(), + MnemonicWithPassphrase::sample_ledger_other(), + ), + ( + FactorSourceIDFromHash::sample_arculus(), + MnemonicWithPassphrase::sample_arculus(), + ), + ( + FactorSourceIDFromHash::sample_arculus_other(), + MnemonicWithPassphrase::sample_arculus_other(), + ), + ( + FactorSourceIDFromHash::sample_password(), + MnemonicWithPassphrase::sample_password(), + ), + ( + FactorSourceIDFromHash::sample_off_device(), + MnemonicWithPassphrase::sample_off_device_other(), + ), + ( + FactorSourceIDFromHash::sample_off_device(), + MnemonicWithPassphrase::sample_off_device(), + ), + ( + FactorSourceIDFromHash::sample_off_device_other(), + MnemonicWithPassphrase::sample_off_device_other(), + ), + ( + FactorSourceIDFromHash::sample_security_questions(), + MnemonicWithPassphrase::sample_security_questions(), + ), + ( + FactorSourceIDFromHash::sample_security_questions_other(), + MnemonicWithPassphrase::sample_security_questions_other(), + ), + ( + FactorSourceIDFromHash::sample_device_other(), + MnemonicWithPassphrase::sample_device_other(), + ), + ( + FactorSourceIDFromHash::sample_device_12_words(), + MnemonicWithPassphrase::sample_device_12_words(), + ), + ( + FactorSourceIDFromHash::sample_device_12_words_other(), + MnemonicWithPassphrase::sample_device_12_words_other(), + ), + ]) +}); + +pub trait MnemonicLookup { + fn sample_associated_mnemonic(&self) -> MnemonicWithPassphrase; +} + +impl MnemonicLookup for FactorSourceIDFromHash { + fn sample_associated_mnemonic(&self) -> MnemonicWithPassphrase { + MNEMONIC_BY_ID_MAP.get(self).cloned().unwrap() + } +} + +impl MnemonicWithPassphraseSamples for MnemonicWithPassphrase { + fn sample_device() -> Self { + Self::with_passphrase(Mnemonic::sample_device(), BIP39Passphrase::default()) + } + + fn sample_device_other() -> Self { + Self::with_passphrase(Mnemonic::sample_device_other(), BIP39Passphrase::default()) + } + + fn sample_device_12_words() -> Self { + Self::with_passphrase( + Mnemonic::sample_device_12_words(), + BIP39Passphrase::default(), + ) + } + + fn sample_device_12_words_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_device_12_words_other(), + BIP39Passphrase::new("Olympia rules!"), + ) + } + + fn sample_ledger() -> Self { + Self::with_passphrase(Mnemonic::sample_ledger(), BIP39Passphrase::default()) + } + + fn sample_ledger_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_ledger_other(), + BIP39Passphrase::new("Mellon"), + ) + } + + fn sample_off_device() -> Self { + Self::with_passphrase(Mnemonic::sample_off_device(), BIP39Passphrase::default()) + } + + fn sample_off_device_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_off_device_other(), + BIP39Passphrase::new("open sesame"), + ) + } + + fn sample_arculus() -> Self { + Self::with_passphrase(Mnemonic::sample_arculus(), BIP39Passphrase::default()) + } + + fn sample_arculus_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_arculus_other(), + BIP39Passphrase::new("Leonidas"), + ) + } + + fn sample_security_questions() -> Self { + Self::with_passphrase( + Mnemonic::sample_security_questions(), + BIP39Passphrase::default(), + ) + } + + fn sample_security_questions_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_security_questions_other(), + BIP39Passphrase::default(), + ) + } + + fn sample_password() -> Self { + Self::with_passphrase(Mnemonic::sample_password(), BIP39Passphrase::default()) + } + + fn sample_password_other() -> Self { + Self::with_passphrase( + Mnemonic::sample_password_other(), + BIP39Passphrase::default(), + ) + } +} diff --git a/crates/rules/src/roles/abstract_role_builder_or_built.rs b/crates/rules/src/roles/abstract_role_builder_or_built.rs new file mode 100644 index 00000000..7958dcc4 --- /dev/null +++ b/crates/rules/src/roles/abstract_role_builder_or_built.rs @@ -0,0 +1,136 @@ +use std::marker::PhantomData; + +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbstractRoleBuilderOrBuilt { + #[serde(skip)] + #[doc(hidden)] + built: PhantomData, + + threshold: u8, + threshold_factors: Vec, + override_factors: Vec, +} + +pub(crate) type AbstractBuiltRoleWithFactor = AbstractRoleBuilderOrBuilt; +pub(crate) type RoleBuilder = AbstractRoleBuilderOrBuilt; + +impl AbstractRoleBuilderOrBuilt { + pub fn role(&self) -> RoleKind { + RoleKind::from_u8(R).expect("RoleKind should be valid") + } + + pub(crate) fn with_factors( + threshold: u8, + threshold_factors: impl IntoIterator, + override_factors: impl IntoIterator, + ) -> Self { + let assert_is_securified = |factors: &Vec| -> Result<(), CommonError> { + let trait_objects: Vec<&dyn IsMaybeKeySpaceAware> = factors + .iter() + .map(|x| x as &dyn IsMaybeKeySpaceAware) + .collect(); + if trait_objects + .iter() + .filter_map(|x| x.maybe_key_space()) + .any(|x| x != KeySpace::Securified) + { + return Err(crate::CommonError::IndexUnsecurifiedExpectedSecurified); + } + Ok(()) + }; + + let threshold_factors = threshold_factors.into_iter().collect(); + let override_factors = override_factors.into_iter().collect(); + + assert_is_securified(&threshold_factors) + .expect("Should not have allowed building of invalid Role"); + assert_is_securified(&override_factors) + .expect("Should not have allowed building of invalid Role"); + + Self { + built: PhantomData, + threshold, + threshold_factors, + override_factors, + } + } +} + +impl AbstractRoleBuilderOrBuilt { + pub fn all_factors(&self) -> Vec<&F> { + self.threshold_factors + .iter() + .chain(self.override_factors.iter()) + .collect() + } + + pub fn get_threshold_factors(&self) -> &Vec { + &self.threshold_factors + } + + pub fn get_override_factors(&self) -> &Vec { + &self.override_factors + } + + pub fn get_threshold(&self) -> u8 { + self.threshold + } +} +pub(crate) const ROLE_PRIMARY: u8 = 1; +pub(crate) const ROLE_RECOVERY: u8 = 2; +pub(crate) const ROLE_CONFIRMATION: u8 = 3; + +pub(crate) trait RoleFromDiscriminator { + fn from_u8(discriminator: u8) -> Option + where + Self: Sized; +} +impl RoleFromDiscriminator for RoleKind { + fn from_u8(discriminator: u8) -> Option { + match discriminator { + ROLE_PRIMARY => Some(RoleKind::Primary), + ROLE_RECOVERY => Some(RoleKind::Recovery), + ROLE_CONFIRMATION => Some(RoleKind::Confirmation), + _ => None, + } + } +} + +impl RoleBuilder { + pub(crate) fn new() -> Self { + Self { + built: PhantomData, + threshold: 0, + threshold_factors: Vec::new(), + override_factors: Vec::new(), + } + } + + pub(crate) fn mut_threshold_factors(&mut self) -> &mut Vec { + &mut self.threshold_factors + } + + pub(crate) fn mut_override_factors(&mut self) -> &mut Vec { + &mut self.override_factors + } + + pub(crate) fn unchecked_add_factor_source_to_list( + &mut self, + factor_source_id: FactorSourceID, + factor_list_kind: FactorListKind, + ) { + match factor_list_kind { + FactorListKind::Threshold => self.threshold_factors.push(factor_source_id), + FactorListKind::Override => self.override_factors.push(factor_source_id), + } + } + + pub(crate) fn unchecked_set_threshold(&mut self, threshold: u8) { + self.threshold = threshold; + } +} diff --git a/crates/rules/src/roles/builder/confirmation_roles_builder_unit_tests.rs b/crates/rules/src/roles/builder/confirmation_roles_builder_unit_tests.rs new file mode 100644 index 00000000..af4c70f1 --- /dev/null +++ b/crates/rules/src/roles/builder/confirmation_roles_builder_unit_tests.rs @@ -0,0 +1,331 @@ +#![cfg(test)] + +use crate::prelude::*; + +#[allow(clippy::upper_case_acronyms)] + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn new_builder_confirmation() { + assert_eq!( + ConfirmationRoleBuilder::new().role(), + RoleKind::Confirmation + ); +} + +#[test] +fn empty_is_err_confirmation() { + let sut = ConfirmationRoleBuilder::new(); + let res = sut.build(); + assert_eq!( + res, + Result::not_yet_valid(NotYetValidReason::RoleMustHaveAtLeastOneFactor) + ); +} + +#[allow(clippy::upper_case_acronyms)] +type SUT = ConfirmationRoleBuilder; + +fn make() -> SUT { + SUT::new() +} + +#[test] +fn validation_for_addition_of_factor_source_of_kind_to_list() { + use FactorSourceKind::*; + let sut = make(); + let not_ok = |kind: FactorSourceKind| { + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_err()); + }; + let ok = |kind: FactorSourceKind| { + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_ok()); + }; + ok(Device); + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(SecurityQuestions); + ok(Password); + ok(OffDeviceMnemonic); + not_ok(TrustedContact); +} + +mod device_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_device_other() + } + + #[test] + fn set_threshold_is_unsupported() { + let mut sut = make(); + assert_eq!( + sut.set_threshold(1), + MutRes::basic_violation(BasicViolation::ConfirmationCannotSetThreshold) + ); + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample()]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + let built = sut.build().unwrap(); + assert!(built.get_threshold_factors().is_empty()); + assert_eq!( + built, + RoleWithFactorSourceIds::confirmation_with_factors([sample(), sample_other()]) + ); + } +} + +mod ledger_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(), sample_other()]) + ); + } +} + +mod arculus_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_arculus_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(), sample_other()]) + ); + } +} + +mod passphrase_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_passphrase() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_passphrase_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(), sample_other()]) + ); + } +} + +mod trusted_contact_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_trusted_contact() + } + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::ConfirmationRoleTrustedContactNotSupported + ) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + + sut.add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::ConfirmationRoleTrustedContactNotSupported + ) + ); + } +} + +mod password_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_password() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_password_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(), sample_other()]) + ); + } +} diff --git a/crates/rules/src/roles/builder/mod.rs b/crates/rules/src/roles/builder/mod.rs new file mode 100644 index 00000000..af9dc07f --- /dev/null +++ b/crates/rules/src/roles/builder/mod.rs @@ -0,0 +1,7 @@ +mod confirmation_roles_builder_unit_tests; +mod primary_roles_builder_unit_tests; +mod recovery_roles_builder_unit_tests; +mod roles_builder; +mod roles_builder_unit_tests; + +pub use roles_builder::*; diff --git a/crates/rules/src/roles/builder/primary_roles_builder_unit_tests.rs b/crates/rules/src/roles/builder/primary_roles_builder_unit_tests.rs new file mode 100644 index 00000000..dee40a53 --- /dev/null +++ b/crates/rules/src/roles/builder/primary_roles_builder_unit_tests.rs @@ -0,0 +1,874 @@ +#![cfg(test)] + +use crate::prelude::*; + +use NotYetValidReason::*; +type Validation = RoleBuilderValidation; + +#[allow(clippy::upper_case_acronyms)] + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn new_builder_primary() { + assert_eq!(PrimaryRoleBuilder::new().role(), RoleKind::Primary); +} + +#[test] +fn empty_is_err_primary() { + let sut = PrimaryRoleBuilder::new(); + let res = sut.build(); + assert_eq!( + res, + Result::not_yet_valid(NotYetValidReason::RoleMustHaveAtLeastOneFactor) + ); +} + +mod primary_test_helper_functions { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleBuilder; + + #[test] + fn factor_sources_not_of_kind_to_list_of_kind_in_override() { + let mut sut = SUT::new(); + sut.add_factor_source_to_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_arculus()) + .unwrap(); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::Device, + FactorListKind::Override, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::LedgerHQHardwareWallet, + FactorListKind::Override, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::ArculusCard, + FactorListKind::Override, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ] + ); + } + + #[test] + fn factor_sources_not_of_kind_to_list_of_kind_in_threshold() { + let mut sut = SUT::new(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_arculus()) + .unwrap(); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::Device, + FactorListKind::Threshold, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::LedgerHQHardwareWallet, + FactorListKind::Threshold, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::ArculusCard, + FactorListKind::Threshold, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ] + ); + } +} + +#[allow(clippy::upper_case_acronyms)] +type SUT = PrimaryRoleBuilder; + +fn make() -> SUT { + SUT::new() +} + +#[cfg(test)] +mod threshold_suite { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_third() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + #[test] + fn remove_lowers_threshold_from_1_to_0() { + let mut sut = make(); + let fs = sample(); + sut.add_factor_source_to_threshold(fs).unwrap(); + sut.set_threshold(1).unwrap(); + assert_eq!(sut.get_threshold(), 1); + assert_eq!( + sut.remove_factor_source(&fs), + Err(Validation::NotYetValid(RoleMustHaveAtLeastOneFactor)) + ); + assert_eq!(sut.get_threshold(), 0); + } + + #[test] + fn remove_lowers_threshold_from_3_to_1() { + let mut sut = make(); + let fs0 = sample(); + let fs1 = sample_other(); + sut.add_factor_source_to_threshold(fs0).unwrap(); + sut.add_factor_source_to_threshold(fs1).unwrap(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_arculus_other()) + .unwrap(); + sut.set_threshold(3).unwrap(); + assert_eq!(sut.get_threshold(), 3); + sut.remove_factor_source(&fs0).unwrap(); + sut.remove_factor_source(&fs1).unwrap(); + assert_eq!(sut.get_threshold(), 1); + } + + #[test] + fn remove_from_override_does_not_change_threshold() { + let mut sut = make(); + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + let fs = FactorSourceID::sample_arculus_other(); + sut.add_factor_source_to_override(fs).unwrap(); + sut.set_threshold(2).unwrap(); + assert_eq!(sut.get_threshold(), 2); + sut.remove_factor_source(&fs).unwrap(); + assert_eq!(sut.get_threshold(), 2); + + let built = sut.build().unwrap(); + assert_eq!(built.get_threshold(), 2); + + assert_eq!(built.role(), RoleKind::Primary); + + assert_eq!( + built.get_threshold_factors(), + &vec![sample(), sample_other()] + ); + + assert_eq!(built.get_override_factors(), &Vec::new()); + } + + #[test] + fn one_factor_then_set_threshold_to_one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(1, [sample_other()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn zero_factor_then_set_threshold_to_one_is_not_yet_valid_then_add_one_factor_is_ok() { + // Arrange + let mut sut = make(); + + // Act + assert_eq!( + sut.set_threshold(1), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(1, [sample_other()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn zero_factor_then_set_threshold_to_two_is_not_yet_valid_then_add_two_factor_is_ok() { + // Arrange + let mut sut = make(); + + // Act + assert_eq!( + sut.set_threshold(2), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + sut.add_factor_source_to_threshold(sample()).unwrap(); + + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Assert + let expected = + RoleWithFactorSourceIds::primary_with_factors(2, [sample(), sample_other()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn add_two_factors_then_set_threshold_to_two_is_ok() { + // Arrange + let mut sut = make(); + + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Act + assert_eq!(sut.set_threshold(2), Ok(())); + + // Assert + let expected = + RoleWithFactorSourceIds::primary_with_factors(2, [sample(), sample_other()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn add_two_factors_then_set_threshold_to_three_is_not_yet_valid_then_add_third_factor_is_ok() { + // Arrange + let mut sut = make(); + + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Act + assert_eq!( + sut.set_threshold(3), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + + sut.add_factor_source_to_threshold(sample_third()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 3, + [sample(), sample_other(), sample_third()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn one_factors_set_threshold_of_one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(1, [sample_other()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn one_override_factors_set_threshold_to_one_is_not_yet_valid() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample_other()).unwrap(); + assert_eq!( + sut.set_threshold(1), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + + // Assert + + assert_eq!( + sut.build(), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_for_each_before_after_adding_a_factor() { + let mut sut = make(); + let fs0 = FactorSourceID::sample_ledger(); + let fs1 = FactorSourceID::sample_password(); + let fs2 = FactorSourceID::sample_arculus(); + let xs = sut.validation_for_addition_of_factor_source_for_each( + FactorListKind::Threshold, + &IndexSet::from_iter([fs0, fs1, fs2]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Primary, fs0,), + FactorSourceInRoleBuilderValidationStatus::not_yet_valid( + RoleKind::Primary, + fs1, + NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor + ), + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Primary, fs2,), + ] + ); + _ = sut.add_factor_source_to_threshold(fs0); + _ = sut.set_threshold(2); + + let xs = sut.validation_for_addition_of_factor_source_for_each( + FactorListKind::Threshold, + &IndexSet::from_iter([fs0, fs1, fs2]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + fs0, + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Primary, fs1,), + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Primary, fs2,), + ] + ); + } +} + +#[cfg(test)] +mod password { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_password() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_password_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + mod threshold_in_isolation { + use super::*; + + #[test] + fn duplicates_not_allowed() { + let mut sut = make(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + _ = sut.set_threshold(2); + test_duplicates_not_allowed(sut, FactorListKind::Threshold, sample()); + } + + #[test] + fn alone_is_not_ok() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source_to_threshold(sample()); + + // Assert + assert_eq!( + res, + MutRes::not_yet_valid( + NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor + ) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_list() { + use FactorSourceKind::*; + + let not_ok = |kind: FactorSourceKind| { + let sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_list( + kind, + FactorListKind::Threshold, + ); + assert!(res.is_err()); + }; + + let ok_with = |kind: FactorSourceKind, setup: fn(&mut SUT)| { + let mut sut = make(); + setup(&mut sut); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_list( + kind, + FactorListKind::Threshold, + ); + assert!(res.is_ok()); + }; + let ok = |kind: FactorSourceKind| { + ok_with(kind, |_| {}); + }; + + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(OffDeviceMnemonic); + + ok_with(Device, |sut| { + sut.add_factor_source_to_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + }); + ok_with(Password, |sut| { + sut.add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + _ = sut.set_threshold(2); + }); + + not_ok(SecurityQuestions); + not_ok(TrustedContact); + } + } + + mod override_in_isolation { + use super::*; + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source_to_override(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::PrimaryCannotHavePasswordInOverrideList + ) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + sut.add_factor_source_to_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source_to_override(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::PrimaryCannotHavePasswordInOverrideList + ) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_override() { + use FactorSourceKind::*; + + let not_ok = |kind: FactorSourceKind| { + let sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_err()); + }; + + let ok_with = |kind: FactorSourceKind, setup: fn(&mut SUT)| { + let mut sut = make(); + setup(&mut sut); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_ok()); + }; + let ok = |kind: FactorSourceKind| { + ok_with(kind, |_| {}); + }; + + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(OffDeviceMnemonic); + + ok_with(Device, |sut| { + sut.add_factor_source_to_override(FactorSourceID::sample_ledger()) + .unwrap(); + }); + + not_ok(Password); + + not_ok(SecurityQuestions); + not_ok(TrustedContact); + } + } +} + +#[cfg(test)] +mod ledger { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + mod threshold_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Threshold + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(1, [sample()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn one_with_threshold_of_zero_is_err() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build(), + Err(RoleBuilderValidation::NotYetValid( + NotYetValidReason::PrimaryRoleWithThresholdCannotBeZeroWithFactors + )) + ); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(2).unwrap(); + + // Assert + let expected = + RoleWithFactorSourceIds::primary_with_factors(2, [sample(), sample_other()], []); + assert_eq!(sut.build().unwrap(), expected); + } + } + + mod override_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Override + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(0, [], [sample()]); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + sut.add_factor_source_to_override(sample_other()).unwrap(); + + // Assert + let expected = + RoleWithFactorSourceIds::primary_with_factors(0, [], [sample(), sample_other()]); + assert_eq!(sut.build().unwrap(), expected); + } + } +} + +#[cfg(test)] +mod arculus { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_arculus_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + mod threshold_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Threshold + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(1, [sample()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = + RoleWithFactorSourceIds::primary_with_factors(1, [sample(), sample_other()], []); + assert_eq!(sut.build().unwrap(), expected); + } + } + + mod override_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Override + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(0, [], [sample()]); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + sut.add_factor_source_to_override(sample_other()).unwrap(); + + // Assert + let expected = + RoleWithFactorSourceIds::primary_with_factors(0, [], [sample(), sample_other()]); + assert_eq!(sut.build().unwrap(), expected); + } + } +} + +#[cfg(test)] +mod device_factor_source { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_device_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + #[cfg(test)] + mod threshold_in_isolation { + use super::*; + + fn list() -> FactorListKind { + FactorListKind::Threshold + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()) + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(1, [sample()], []); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_err() { + // Arrange + let mut sut = make(); + + sut.add_factor_source_to_threshold(sample()).unwrap(); + + // Act + let res = sut.add_factor_source_to_threshold(sample_other()); + + // Assert + assert!(matches!( + res, + MutRes::Err(Validation::ForeverInvalid( + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + )) + )); + } + } + + mod override_in_isolation { + + use super::*; + + fn list() -> FactorListKind { + FactorListKind::Override + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()) + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors(0, [], [sample()]); + assert_eq!(sut.build().unwrap(), expected); + } + } +} diff --git a/crates/rules/src/roles/builder/recovery_roles_builder_unit_tests.rs b/crates/rules/src/roles/builder/recovery_roles_builder_unit_tests.rs new file mode 100644 index 00000000..d2fd4322 --- /dev/null +++ b/crates/rules/src/roles/builder/recovery_roles_builder_unit_tests.rs @@ -0,0 +1,398 @@ +#![cfg(test)] + +use crate::prelude::*; + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn new_builder_recovery() { + assert_eq!(RecoveryRoleBuilder::new().role(), RoleKind::Recovery); +} + +#[test] +fn empty_is_err_recovery() { + let sut = RecoveryRoleBuilder::new(); + let res = sut.build(); + assert_eq!( + res, + Result::not_yet_valid(NotYetValidReason::RoleMustHaveAtLeastOneFactor) + ); +} + +#[allow(clippy::upper_case_acronyms)] +type SUT = RecoveryRoleBuilder; + +fn make() -> SUT { + SUT::new() +} + +fn list() -> FactorListKind { + FactorListKind::Override +} + +#[test] +fn validation_for_addition_of_factor_source_of_kind_to_list() { + use FactorSourceKind::*; + let sut = make(); + let not_ok = |kind: FactorSourceKind| { + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_err()); + }; + let ok = |kind: FactorSourceKind| { + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_ok()); + }; + ok(Device); + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(TrustedContact); + ok(OffDeviceMnemonic); + + not_ok(Password); + not_ok(SecurityQuestions); +} + +#[test] +fn set_threshold_is_unsupported() { + let mut sut = make(); + assert_eq!( + sut.set_threshold(1), + MutRes::basic_violation(BasicViolation::RecoveryCannotSetThreshold) + ); +} + +mod device_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_device_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample()]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(), sample_other()],) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_for_each() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_for_each( + list(), + &IndexSet::from_iter([sample(), sample_other()]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Recovery, sample()), + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Recovery, sample_other(),) + ] + ); + } +} + +mod ledger_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample()],) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(), sample_other()]) + ); + } +} + +mod arculus_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_arculus_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(), sample_other()]) + ); + } +} + +mod passphrase_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_passphrase() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_passphrase_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample()]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(), sample_other()]) + ); + } +} + +mod trusted_contact_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_trusted_contact() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_trusted_contact_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // TODO: Ask Matt + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(), sample_other()]) + ); + } +} + +mod password_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_password() + } + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid(ForeverInvalidReason::RecoveryRolePasswordNotSupported) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + + sut.add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid(ForeverInvalidReason::RecoveryRolePasswordNotSupported) + ); + } +} + +mod security_questions_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_security_questions() + } + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_security_questions_other() + } + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::RecoveryRoleSecurityQuestionsNotSupported + ) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + + sut.add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source(sample_other()); + + // Assert + let reason = ForeverInvalidReason::RecoveryRoleSecurityQuestionsNotSupported; + let err = MutRes::forever_invalid(reason); + assert_eq!(res, err); + + // .. erroneous action above did not change the state of the builder (SUT), + // so we can build and `sample` is not present in the built result. + assert_eq!( + sut.build(), + Ok(RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ])) + ); + } +} diff --git a/crates/rules/src/roles/builder/roles_builder.rs b/crates/rules/src/roles/builder/roles_builder.rs new file mode 100644 index 00000000..b10e6ef0 --- /dev/null +++ b/crates/rules/src/roles/builder/roles_builder.rs @@ -0,0 +1,778 @@ +use crate::prelude::*; + +use FactorListKind::*; + +pub type PrimaryRoleBuilder = RoleBuilder<{ ROLE_PRIMARY }>; +pub type RecoveryRoleBuilder = RoleBuilder<{ ROLE_RECOVERY }>; +pub type ConfirmationRoleBuilder = RoleBuilder<{ ROLE_CONFIRMATION }>; + +#[cfg(test)] +impl PrimaryRoleWithFactorSourceIds { + pub(crate) fn primary_with_factors( + threshold: u8, + threshold_factors: impl IntoIterator, + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(threshold, threshold_factors, override_factors) + } +} + +#[cfg(test)] +impl RecoveryRoleWithFactorSourceIds { + pub(crate) fn recovery_with_factors( + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(0, vec![], override_factors) + } +} + +#[cfg(test)] +impl ConfirmationRoleWithFactorSourceIds { + pub(crate) fn confirmation_with_factors( + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(0, vec![], override_factors) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum RoleBuilderValidation { + #[error("Basic violation: {0}")] + BasicViolation(#[from] BasicViolation), + + #[error("Forever invalid: {0}")] + ForeverInvalid(#[from] ForeverInvalidReason), + + #[error("Not yet valid: {0}")] + NotYetValid(#[from] NotYetValidReason), +} +use RoleBuilderValidation::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum BasicViolation { + /// e.g. tried to remove a factor source which was not found. + #[error("FactorSourceID not found")] + FactorSourceNotFound, + + #[error("Recovery cannot set threshold")] + RecoveryCannotSetThreshold, + + #[error("Confirmation cannot set threshold")] + ConfirmationCannotSetThreshold, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum NotYetValidReason { + #[error("Role must have at least one factor")] + RoleMustHaveAtLeastOneFactor, + + #[error("Primary role with password in threshold list must have another factor")] + PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor, + + #[error("Primary role with threshold factors cannot have a threshold of zero")] + PrimaryRoleWithThresholdCannotBeZeroWithFactors, + + #[error("Primary role with password in threshold list must have threshold greater than one")] + PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne, + + #[error("Threshold higher than threshold factors len")] + ThresholdHigherThanThresholdFactorsLen, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum ForeverInvalidReason { + #[error("Factor source already present")] + FactorSourceAlreadyPresent, + + #[error("Primary role cannot have multiple devices")] + PrimaryCannotHaveMultipleDevices, + + #[error("Primary role cannot have password in override list")] + PrimaryCannotHavePasswordInOverrideList, + + #[error("Primary role cannot contain Security Questions")] + PrimaryCannotContainSecurityQuestions, + + #[error("Primary role cannot contain Trusted Contact")] + PrimaryCannotContainTrustedContact, + + #[error("Recovery role threshold list not supported")] + RecoveryRoleThresholdFactorsNotSupported, + + #[error("Recovery role Security Questions not supported")] + RecoveryRoleSecurityQuestionsNotSupported, + + #[error("Recovery role password not supported")] + RecoveryRolePasswordNotSupported, + + #[error("Confirmation role threshold list not supported")] + ConfirmationRoleThresholdFactorsNotSupported, + + #[error("Confirmation role cannot contain Trusted Contact")] + ConfirmationRoleTrustedContactNotSupported, +} + +pub(crate) trait FromForeverInvalid { + fn forever_invalid(reason: ForeverInvalidReason) -> Self; +} +impl FromForeverInvalid for std::result::Result { + fn forever_invalid(reason: ForeverInvalidReason) -> Self { + Err(ForeverInvalid(reason)) + } +} + +pub(crate) trait FromNotYetValid { + fn not_yet_valid(reason: NotYetValidReason) -> Self; +} +impl FromNotYetValid for std::result::Result { + fn not_yet_valid(reason: NotYetValidReason) -> Self { + Err(NotYetValid(reason)) + } +} + +pub(crate) trait FromBasicViolation { + fn basic_violation(reason: BasicViolation) -> Self; +} +impl FromBasicViolation for std::result::Result { + fn basic_violation(reason: BasicViolation) -> Self { + Err(BasicViolation(reason)) + } +} + +impl ForeverInvalidReason { + pub(crate) fn threshold_list_not_supported_for_role(role: RoleKind) -> Self { + match role { + RoleKind::Recovery => Self::RecoveryRoleThresholdFactorsNotSupported, + RoleKind::Confirmation => Self::ConfirmationRoleThresholdFactorsNotSupported, + RoleKind::Primary => { + unreachable!("Primary role DOES support threshold list. This is programmer error.") + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FactorSourceInRoleBuilderValidationStatus { + pub role: RoleKind, + pub factor_source_id: FactorSourceID, + pub validation: RoleBuilderMutateResult, +} + +impl FactorSourceInRoleBuilderValidationStatus { + pub(crate) fn new( + role: RoleKind, + factor_source_id: FactorSourceID, + validation: RoleBuilderMutateResult, + ) -> Self { + Self { + role, + factor_source_id, + validation, + } + } +} + +#[cfg(test)] +impl FactorSourceInRoleBuilderValidationStatus { + pub(crate) fn ok(role: RoleKind, factor_source_id: FactorSourceID) -> Self { + Self::new(role, factor_source_id, Ok(())) + } + + pub(crate) fn forever_invalid( + role: RoleKind, + factor_source_id: FactorSourceID, + reason: ForeverInvalidReason, + ) -> Self { + Self::new( + role, + factor_source_id, + RoleBuilderMutateResult::forever_invalid(reason), + ) + } + + pub(crate) fn not_yet_valid( + role: RoleKind, + factor_source_id: FactorSourceID, + reason: NotYetValidReason, + ) -> Self { + Self::new( + role, + factor_source_id, + RoleBuilderMutateResult::not_yet_valid(reason), + ) + } +} + +use BasicViolation::*; +use ForeverInvalidReason::*; +use NotYetValidReason::*; +use RoleKind::*; + +pub type RoleBuilderMutateResult = Result<(), RoleBuilderValidation>; + +pub enum Assert {} +pub trait IsTrue {} +impl IsTrue for Assert {} + +impl RoleBuilder +where + Assert<{ R == ROLE_PRIMARY }>: IsTrue, +{ + /// If Ok => self is mutated + /// If Err(NotYetValid) => self is mutated + /// If Err(ForeverInvalid) => self is not mutated + pub(crate) fn add_factor_source_to_threshold( + &mut self, + factor_source_id: FactorSourceID, + ) -> RoleBuilderMutateResult { + self._add_factor_source_to_list(factor_source_id, Threshold) + } + + /// If we would add a factor of kind `factor_source_kind` to the list of kind `factor_list_kind` + /// what would be the validation status? + pub(crate) fn validation_for_addition_of_factor_source_of_kind_to_threshold( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self._validation_add(factor_source_kind, Threshold) + } + + #[cfg(test)] + pub(crate) fn validation_for_addition_of_factor_source_of_kind_to_list( + &self, + factor_source_kind: FactorSourceKind, + list: FactorListKind, + ) -> RoleBuilderMutateResult { + self._validation_add(factor_source_kind, list) + } +} + +impl RoleBuilder +where + Assert<{ R > ROLE_PRIMARY }>: IsTrue, +{ + /// If Ok => self is mutated + /// If Err(NotYetValid) => self is mutated + /// If Err(ForeverInvalid) => self is not mutated + pub(crate) fn add_factor_source( + &mut self, + factor_source_id: FactorSourceID, + ) -> RoleBuilderMutateResult { + self.add_factor_source_to_override(factor_source_id) + } +} + +impl RoleBuilder { + /// If Ok => self is mutated + /// If Err(NotYetValid) => self is mutated + /// If Err(ForeverInvalid) => self is not mutated + pub(crate) fn add_factor_source_to_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> RoleBuilderMutateResult { + self._add_factor_source_to_list(factor_source_id, Override) + } + + /// If Ok => self is mutated + /// If Err(NotYetValid) => self is mutated + /// If Err(ForeverInvalid) => self is not mutated + fn _add_factor_source_to_list( + &mut self, + factor_source_id: FactorSourceID, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + let validation = self + .validation_for_addition_of_factor_source_to_list(&factor_source_id, factor_list_kind); + match validation.as_ref() { + Ok(()) | Err(NotYetValid(_)) => { + self.unchecked_add_factor_source_to_list(factor_source_id, factor_list_kind); + } + Err(ForeverInvalid(_)) | Err(BasicViolation(_)) => {} + } + validation + } + + /// If we would add a factor of kind `factor_source_kind` to the list of kind `factor_list_kind` + /// what would be the validation status? + pub(crate) fn validation_for_addition_of_factor_source_of_kind_to_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self._validation_add(factor_source_kind, Override) + } + + /// If we would add a factor of kind `factor_source_kind` to the list of kind `factor_list_kind` + /// what would be the validation status? + fn _validation_add( + &self, + factor_source_kind: FactorSourceKind, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + match self.role() { + RoleKind::Primary => { + return self.validation_for_addition_of_factor_source_of_kind_to_list_for_primary( + factor_source_kind, + factor_list_kind, + ) + } + RoleKind::Recovery | RoleKind::Confirmation => match factor_list_kind { + Threshold => { + return Result::forever_invalid( + ForeverInvalidReason::threshold_list_not_supported_for_role(self.role()), + ) + } + Override => {} + }, + } + self.validation_for_addition_of_factor_source_of_kind_to_override_for_non_primary_role( + factor_source_kind, + ) + } +} + +impl RoleBuilder { + pub(crate) fn build(self) -> Result, RoleBuilderValidation> { + self.validate().map(|_| { + RoleWithFactorSourceIds::with_factors( + self.get_threshold(), + self.get_threshold_factors().clone(), + self.get_override_factors().clone(), + ) + }) + } + + #[allow(dead_code)] + pub(crate) fn set_threshold(&mut self, threshold: u8) -> RoleBuilderMutateResult { + match self.role() { + Primary => { + self.unchecked_set_threshold(threshold); + self.validate() + } + Recovery => RoleBuilderMutateResult::basic_violation(RecoveryCannotSetThreshold), + Confirmation => { + RoleBuilderMutateResult::basic_violation(ConfirmationCannotSetThreshold) + } + } + } + + fn override_contains_factor_source(&self, factor_source_id: &FactorSourceID) -> bool { + self.get_override_factors().contains(factor_source_id) + } + + fn threshold_contains_factor_source(&self, factor_source_id: &FactorSourceID) -> bool { + self.get_threshold_factors().contains(factor_source_id) + } + + fn override_contains_factor_source_of_kind( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get_override_factors() + .iter() + .any(|f| f.get_factor_source_kind() == factor_source_kind) + } + + fn threshold_contains_factor_source_of_kind( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get_threshold_factors() + .iter() + .any(|f| f.get_factor_source_kind() == factor_source_kind) + } + + /// Validates `self` by "replaying" the addition of each factor source in `self` to a + /// "simulation" (clone). If the simulation is valid, then `self` is valid. + pub(crate) fn validate(&self) -> RoleBuilderMutateResult { + let mut simulation = Self::new(); + + // Validate override factors + for f in self.get_override_factors() { + let validation = simulation.add_factor_source_to_override(*f); + match validation.as_ref() { + Ok(()) | Err(NotYetValid(_)) => continue, + Err(ForeverInvalid(_)) | Err(BasicViolation(_)) => return validation, + } + } + + // Validate threshold factors + for f in self.get_threshold_factors() { + let validation = simulation._add_factor_source_to_list(*f, Threshold); + match validation.as_ref() { + Ok(()) | Err(NotYetValid(_)) => continue, + Err(ForeverInvalid(_)) | Err(BasicViolation(_)) => return validation, + } + } + + // Validate threshold count + if self.role() == RoleKind::Primary { + if self.get_threshold_factors().len() < self.get_threshold() as usize { + return RoleBuilderMutateResult::not_yet_valid( + NotYetValidReason::ThresholdHigherThanThresholdFactorsLen, + ); + } + if self.get_threshold() == 0 && !self.get_threshold_factors().is_empty() { + return RoleBuilderMutateResult::not_yet_valid( + NotYetValidReason::PrimaryRoleWithThresholdCannotBeZeroWithFactors, + ); + } + } else if self.get_threshold() != 0 { + match self.role() { + Primary => unreachable!("Primary role should have been handled earlier"), + Recovery => { + return RoleBuilderMutateResult::basic_violation(RecoveryCannotSetThreshold) + } + Confirmation => { + return RoleBuilderMutateResult::basic_violation(ConfirmationCannotSetThreshold) + } + } + } + + if self.all_factors().is_empty() { + return RoleBuilderMutateResult::not_yet_valid(RoleMustHaveAtLeastOneFactor); + } + + Ok(()) + } + + fn validation_for_addition_of_factor_source_of_kind_to_override_for_non_primary_role( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + match self.role() { + RoleKind::Primary => { + unreachable!("Should have branched to 'primary' earlier, this is programmer error.") + } + RoleKind::Confirmation => self + .validation_for_addition_of_factor_source_of_kind_to_override_for_confirmation( + factor_source_kind, + ), + RoleKind::Recovery => self + .validation_for_addition_of_factor_source_of_kind_to_override_for_recovery( + factor_source_kind, + ), + } + } + + #[allow(dead_code)] + /// For each factor source in the given set, return a validation status + /// for adding it to factor list of the given kind (`factor_list_kind`) + pub(crate) fn validation_for_addition_of_factor_source_for_each( + &self, + factor_list_kind: FactorListKind, + factor_sources: &IndexSet, + ) -> IndexSet { + factor_sources + .iter() + .map(|factor_source_id| { + let validation_status = self.validation_for_addition_of_factor_source_to_list( + factor_source_id, + factor_list_kind, + ); + FactorSourceInRoleBuilderValidationStatus::new( + self.role(), + *factor_source_id, + validation_status, + ) + }) + .collect() + } + + fn validation_for_addition_of_factor_source_to_list( + &self, + factor_source_id: &FactorSourceID, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + if self.contains_factor_source(factor_source_id) { + return RoleBuilderMutateResult::forever_invalid(FactorSourceAlreadyPresent); + } + let factor_source_kind = factor_source_id.get_factor_source_kind(); + self._validation_add(factor_source_kind, factor_list_kind) + } + + fn contains_factor_source(&self, factor_source_id: &FactorSourceID) -> bool { + self.override_contains_factor_source(factor_source_id) + || self.threshold_contains_factor_source(factor_source_id) + } + + fn contains_factor_source_of_kind(&self, factor_source_kind: FactorSourceKind) -> bool { + self.override_contains_factor_source_of_kind(factor_source_kind) + || self.threshold_contains_factor_source_of_kind(factor_source_kind) + } + + /// Lowers the threshold if the deleted factor source is in the threshold list + /// and if after removal of `factor_source_id` `self.threshold > self.threshold_factors.len()` + /// + /// Returns `Ok` if `factor_source_id` was found and deleted. However, does not call `self.validate()`, + /// So state might still be invalid, i.e. we return the result of the action of removal, not the + /// state validation status. + pub(crate) fn remove_factor_source( + &mut self, + factor_source_id: &FactorSourceID, + ) -> RoleBuilderMutateResult { + if !self.contains_factor_source(factor_source_id) { + return RoleBuilderMutateResult::basic_violation(FactorSourceNotFound); + } + let remove = |xs: &mut Vec| { + let index = xs + .iter() + .position(|f| f == factor_source_id) + .expect("Called remove of non existing FactorSourceID, this is a programmer error, should have checked if it exists before calling remove."); + xs.remove(index); + }; + + if self.override_contains_factor_source(factor_source_id) { + remove(self.mut_override_factors()) + } + if self.threshold_contains_factor_source(factor_source_id) { + remove(self.mut_threshold_factors()); + let threshold_factors_len = self.get_threshold_factors().len() as u8; + if self.get_threshold() > threshold_factors_len { + self.set_threshold(threshold_factors_len)?; + } + } + + Ok(()) + } + + #[cfg(not(tarpaulin_include))] // false negative + fn validation_for_addition_of_factor_source_of_kind_to_list_for_primary( + &self, + factor_source_kind: FactorSourceKind, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + match factor_source_kind { + FactorSourceKind::Password => { + return self.validation_for_addition_of_password_to_primary(factor_list_kind) + } + FactorSourceKind::SecurityQuestions => { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotContainSecurityQuestions, + ); + } + FactorSourceKind::TrustedContact => { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotContainTrustedContact, + ); + } + FactorSourceKind::Device => { + if self.contains_factor_source_of_kind(FactorSourceKind::Device) { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotHaveMultipleDevices, + ); + } + } + FactorSourceKind::LedgerHQHardwareWallet + | FactorSourceKind::ArculusCard + | FactorSourceKind::OffDeviceMnemonic => {} + } + Ok(()) + } + + #[cfg(not(tarpaulin_include))] // false negative + fn validation_for_addition_of_factor_source_of_kind_to_override_for_confirmation( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + assert_eq!(self.role(), RoleKind::Confirmation); + match factor_source_kind { + FactorSourceKind::Device + | FactorSourceKind::LedgerHQHardwareWallet + | FactorSourceKind::ArculusCard + | FactorSourceKind::Password + | FactorSourceKind::OffDeviceMnemonic + | FactorSourceKind::SecurityQuestions => Ok(()), + FactorSourceKind::TrustedContact => { + RoleBuilderMutateResult::forever_invalid(ConfirmationRoleTrustedContactNotSupported) + } + } + } + + #[cfg(not(tarpaulin_include))] // false negative + fn validation_for_addition_of_factor_source_of_kind_to_override_for_recovery( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + assert_eq!(self.role(), RoleKind::Recovery); + match factor_source_kind { + FactorSourceKind::Device + | FactorSourceKind::LedgerHQHardwareWallet + | FactorSourceKind::ArculusCard + | FactorSourceKind::OffDeviceMnemonic + | FactorSourceKind::TrustedContact => Ok(()), + FactorSourceKind::SecurityQuestions => { + RoleBuilderMutateResult::forever_invalid(RecoveryRoleSecurityQuestionsNotSupported) + } + FactorSourceKind::Password => { + RoleBuilderMutateResult::forever_invalid(RecoveryRolePasswordNotSupported) + } + } + } +} + +// ======================= +// ======== RULES ======== +// ======================= +impl RoleBuilder { + fn validation_for_addition_of_password_to_primary( + &self, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + assert_eq!(self.role(), RoleKind::Primary); + let factor_source_kind = FactorSourceKind::Password; + match factor_list_kind { + Threshold => { + let is_alone = self + .factor_sources_not_of_kind_to_list_of_kind(factor_source_kind, Threshold) + .is_empty(); + if is_alone { + return RoleBuilderMutateResult::not_yet_valid( + PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor, + ); + } + if self.get_threshold() < 2 { + return RoleBuilderMutateResult::not_yet_valid( + PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne, + ); + } + } + Override => { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotHavePasswordInOverrideList, + ); + } + } + + Ok(()) + } + + pub(crate) fn factor_sources_not_of_kind_to_list_of_kind( + &self, + factor_source_kind: FactorSourceKind, + factor_list_kind: FactorListKind, + ) -> Vec { + let filter = |xs: &Vec| -> Vec { + xs.iter() + .filter(|f| f.get_factor_source_kind() != factor_source_kind) + .cloned() + .collect() + }; + match factor_list_kind { + Override => filter(self.get_override_factors()), + Threshold => filter(self.get_threshold_factors()), + } + } +} + +#[cfg(test)] +pub(crate) fn test_duplicates_not_allowed( + sut: RoleBuilder, + list: FactorListKind, + factor_source_id: FactorSourceID, +) { + // Arrange + let mut sut = sut; + + sut._add_factor_source_to_list(factor_source_id, list) + .unwrap(); + + // Act + let res = sut._add_factor_source_to_list( + factor_source_id, // oh no, duplicate! + list, + ); + + // Assert + assert!(matches!( + res, + RoleBuilderMutateResult::Err(ForeverInvalid( + ForeverInvalidReason::FactorSourceAlreadyPresent + )) + )); +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn primary_duplicates_not_allowed() { + test_duplicates_not_allowed( + PrimaryRoleBuilder::new(), + Override, + FactorSourceID::sample_arculus(), + ); + test_duplicates_not_allowed( + PrimaryRoleBuilder::new(), + Threshold, + FactorSourceID::sample_arculus(), + ); + } + + #[test] + fn recovery_duplicates_not_allowed() { + test_duplicates_not_allowed( + RecoveryRoleBuilder::new(), + Override, + FactorSourceID::sample_arculus(), + ); + } + + #[test] + fn confirmation_duplicates_not_allowed() { + test_duplicates_not_allowed( + ConfirmationRoleBuilder::new(), + Override, + FactorSourceID::sample_arculus(), + ); + } + + #[test] + fn recovery_cannot_add_factors_to_threshold() { + let mut sut = RecoveryRoleBuilder::new(); + let res = sut._add_factor_source_to_list(FactorSourceID::sample_ledger(), Threshold); + assert_eq!( + res, + Err(ForeverInvalid( + ForeverInvalidReason::RecoveryRoleThresholdFactorsNotSupported + )) + ); + } + + #[test] + fn confirmation_cannot_add_factors_to_threshold() { + let mut sut = ConfirmationRoleBuilder::new(); + let res = sut._add_factor_source_to_list(FactorSourceID::sample_ledger(), Threshold); + assert_eq!( + res, + Err(ForeverInvalid( + ForeverInvalidReason::ConfirmationRoleThresholdFactorsNotSupported + )) + ); + } + + #[test] + fn recovery_validation_add_is_err_for_threshold() { + let sut = RecoveryRoleBuilder::new(); + let res = sut._validation_add(FactorSourceKind::Device, Threshold); + assert_eq!( + res, + RoleBuilderMutateResult::forever_invalid( + ForeverInvalidReason::threshold_list_not_supported_for_role(RoleKind::Recovery) + ) + ); + } + + #[test] + fn confirmation_validation_add_is_err_for_threshold() { + let sut = ConfirmationRoleBuilder::new(); + let res = sut._validation_add(FactorSourceKind::Device, Threshold); + assert_eq!( + res, + RoleBuilderMutateResult::forever_invalid( + ForeverInvalidReason::threshold_list_not_supported_for_role(RoleKind::Confirmation) + ) + ); + } +} diff --git a/crates/rules/src/roles/builder/roles_builder_unit_tests.rs b/crates/rules/src/roles/builder/roles_builder_unit_tests.rs new file mode 100644 index 00000000..ab92f399 --- /dev/null +++ b/crates/rules/src/roles/builder/roles_builder_unit_tests.rs @@ -0,0 +1,72 @@ +#![cfg(test)] + +use crate::prelude::*; + +use NotYetValidReason::*; + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn validate_override_for_ever_invalid() { + let sut = PrimaryRoleBuilder::with_factors( + 0, + vec![], + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger(), + ], + ); + let res = sut.validate(); + assert_eq!( + res, + MutRes::forever_invalid(ForeverInvalidReason::FactorSourceAlreadyPresent) + ); +} + +#[test] +fn validate_threshold_for_ever_invalid() { + let sut = PrimaryRoleBuilder::with_factors( + 1, + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger(), + ], + vec![], + ); + let res = sut.validate(); + assert_eq!( + res, + MutRes::forever_invalid(ForeverInvalidReason::FactorSourceAlreadyPresent) + ); +} + +#[test] +fn confirmation_validate_basic_violation() { + let sut = + ConfirmationRoleBuilder::with_factors(1, vec![], vec![FactorSourceID::sample_ledger()]); + let res = sut.validate(); + assert_eq!( + res, + MutRes::basic_violation(BasicViolation::ConfirmationCannotSetThreshold) + ); +} + +#[test] +fn recovery_validate_basic_violation() { + let sut = RecoveryRoleBuilder::with_factors(1, vec![], vec![FactorSourceID::sample_ledger()]); + let res = sut.validate(); + assert_eq!( + res, + MutRes::basic_violation(BasicViolation::RecoveryCannotSetThreshold) + ); +} + +#[test] +fn primary_validate_not_yet_valid_for_threshold_greater_than_threshold_factors() { + let sut = PrimaryRoleBuilder::with_factors(1, vec![], vec![FactorSourceID::sample_ledger()]); + let res = sut.validate(); + assert_eq!( + res, + MutRes::not_yet_valid(ThresholdHigherThanThresholdFactorsLen) + ); +} diff --git a/crates/rules/src/roles/factor_levels/factor_instance_level/confirmation_role_with_factor_instances.rs b/crates/rules/src/roles/factor_levels/factor_instance_level/confirmation_role_with_factor_instances.rs new file mode 100644 index 00000000..179d18f1 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_instance_level/confirmation_role_with_factor_instances.rs @@ -0,0 +1,33 @@ +use crate::prelude::*; + +pub(crate) type ConfirmationRoleWithFactorInstances = + RoleWithFactorInstances<{ ROLE_CONFIRMATION }>; + +impl HasSampleValues for ConfirmationRoleWithFactorInstances { + fn sample() -> Self { + MatrixOfFactorInstances::sample().confirmation_role + } + + fn sample_other() -> Self { + MatrixOfFactorInstances::sample_other().confirmation_role + } +} + +#[cfg(test)] +mod confirmation_tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = ConfirmationRoleWithFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_instance_level/general_role_with_hierarchical_deterministic_factor_instances.rs b/crates/rules/src/roles/factor_levels/factor_instance_level/general_role_with_hierarchical_deterministic_factor_instances.rs new file mode 100644 index 00000000..1b8dcdad --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_instance_level/general_role_with_hierarchical_deterministic_factor_instances.rs @@ -0,0 +1,264 @@ +use crate::prelude::*; + +/// A general depiction of each of the roles in a `MatrixOfFactorInstances`. +/// `SignaturesCollector` can work on any `RoleKind` when dealing with a securified entity. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct GeneralRoleWithHierarchicalDeterministicFactorInstances { + role: RoleKind, + threshold: u8, + threshold_factors: Vec, + override_factors: Vec, +} + +impl GeneralRoleWithHierarchicalDeterministicFactorInstances { + pub fn with_factors_and_role( + role: RoleKind, + threshold_factors: impl IntoIterator, + threshold: u8, + override_factors: impl IntoIterator, + ) -> Result { + let threshold_factors = threshold_factors.into_iter().collect_vec(); + let override_factors = override_factors.into_iter().collect_vec(); + + // validate + let _ = PrimaryRoleWithFactorInstances::with_factors( + threshold, + threshold_factors + .clone() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + override_factors + .clone() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + ); + + Ok(Self { + role, + threshold, + threshold_factors, + override_factors, + }) + } +} + +impl HasRoleKindObjectSafe for GeneralRoleWithHierarchicalDeterministicFactorInstances { + fn get_role_kind(&self) -> RoleKind { + self.role + } +} + +impl TryFrom<(MatrixOfFactorInstances, RoleKind)> + for GeneralRoleWithHierarchicalDeterministicFactorInstances +{ + type Error = CommonError; + + fn try_from( + (matrix, role_kind): (MatrixOfFactorInstances, RoleKind), + ) -> Result { + let threshold_factors: Vec; + let override_factors: Vec; + let threshold: u8; + + match role_kind { + RoleKind::Primary => { + let role = matrix.primary(); + threshold = role.get_threshold(); + threshold_factors = role.get_threshold_factors().clone(); + override_factors = role.get_override_factors().clone(); + } + RoleKind::Recovery => { + let role = matrix.recovery(); + threshold = role.get_threshold(); + threshold_factors = role.get_threshold_factors().clone(); + override_factors = role.get_override_factors().clone(); + } + RoleKind::Confirmation => { + let role = matrix.confirmation(); + threshold = role.get_threshold(); + threshold_factors = role.get_threshold_factors().clone(); + override_factors = role.get_override_factors().clone(); + } + } + + Self::with_factors_and_role( + role_kind, + threshold_factors + .iter() + .map(|f| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone()) + }) + .collect::, CommonError>>()?, + threshold, + override_factors + .iter() + .map(|f| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone()) + }) + .collect::, CommonError>>()?, + ) + } +} + +impl GeneralRoleWithHierarchicalDeterministicFactorInstances { + pub fn single_override( + role: RoleKind, + factor: HierarchicalDeterministicFactorInstance, + ) -> Self { + assert!(factor.is_securified(), "non securified factor"); + Self::with_factors_and_role(role, [], 0, [factor]) + .expect("Zero threshold with zero threshold factors and one override should not fail.") + } + + pub fn single_threshold( + role: RoleKind, + factor: HierarchicalDeterministicFactorInstance, + ) -> Self { + assert!(factor.is_securified(), "non securified factor"); + Self::with_factors_and_role(role, [factor], 1, []) + .expect("Single threshold with one threshold factor should not fail.") + } +} + +impl HasSampleValues for GeneralRoleWithHierarchicalDeterministicFactorInstances { + fn sample() -> Self { + Self::try_from((MatrixOfFactorInstances::sample(), RoleKind::Primary)) + .expect("Sample should not fail") + } + + fn sample_other() -> Self { + Self::try_from((MatrixOfFactorInstances::sample_other(), RoleKind::Recovery)) + .expect("Sample should not fail") + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = GeneralRoleWithHierarchicalDeterministicFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + fn matrix() -> MatrixOfFactorInstances { + MatrixOfFactorInstances::sample() + } + + #[test] + fn test_from_primary_role() { + pretty_assertions::assert_eq!( + SUT::try_from( + (matrix(), RoleKind::Primary) + ).unwrap(), + SUT::with_factors_and_role( + RoleKind::Primary, + [ + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0), + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_1_securified_at_index(0) + ], + 2, + [] + ).unwrap() + ) + } + + #[test] + fn test_single_threshold() { + pretty_assertions::assert_eq!( + SUT::single_threshold(RoleKind::Primary, HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_1_securified_at_index(0)), + SUT::with_factors_and_role( + RoleKind::Primary, + [ + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_1_securified_at_index(0) + ], + 1, + [] + ).unwrap() + ) + } + + #[test] + fn test_get_role() { + let test = |role: RoleKind| { + let sut = SUT::single_override( + role, + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0) + ); + assert_eq!(sut.get_role_kind(), role); + }; + test(RoleKind::Primary); + test(RoleKind::Confirmation); + test(RoleKind::Recovery); + } + + #[test] + fn test_from_recovery_role() { + let m = matrix(); + let r = m.recovery(); + assert_eq!( + SUT::try_from((matrix(), RoleKind::Recovery)).unwrap(), + SUT::with_factors_and_role( + RoleKind::Recovery, + [], + 0, + r.get_override_factors() + .clone() + .into_iter() + .map(|f: FactorInstance| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f) + .unwrap() + }) + .collect_vec(), + ) + .unwrap() + ) + } + + #[test] + fn test_from_confirmation_role() { + let m = matrix(); + let r = m.confirmation(); + assert_eq!( + SUT::try_from((matrix(), RoleKind::Confirmation)).unwrap(), + SUT::with_factors_and_role( + RoleKind::Confirmation, + [], + 0, + r.get_override_factors() + .clone() + .into_iter() + .map(|f: FactorInstance| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f) + .unwrap() + }) + .collect_vec(), + ) + .unwrap() + ) + } + + #[test] + fn test_from_matrix_containing_physical_badge() { + let mut matrix = MatrixOfFactorInstances::sample(); + matrix.primary_role = + PrimaryRoleWithFactorInstances::with_factors(0, [], [FactorInstance::sample_other()]); + + assert_eq!( + SUT::try_from((matrix, RoleKind::Primary)), + Err(CommonError::BadgeIsNotVirtualHierarchicalDeterministic) + ); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_instance_level/mod.rs b/crates/rules/src/roles/factor_levels/factor_instance_level/mod.rs new file mode 100644 index 00000000..ca98a01d --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_instance_level/mod.rs @@ -0,0 +1,11 @@ +mod confirmation_role_with_factor_instances; +mod general_role_with_hierarchical_deterministic_factor_instances; +mod primary_role_with_factor_instances; +mod recovery_role_with_factor_instances; +mod role_with_factor_instances; + +pub(crate) use confirmation_role_with_factor_instances::*; +pub use general_role_with_hierarchical_deterministic_factor_instances::*; +pub(crate) use primary_role_with_factor_instances::*; +pub(crate) use recovery_role_with_factor_instances::*; +pub(crate) use role_with_factor_instances::*; diff --git a/crates/rules/src/roles/factor_levels/factor_instance_level/primary_role_with_factor_instances.rs b/crates/rules/src/roles/factor_levels/factor_instance_level/primary_role_with_factor_instances.rs new file mode 100644 index 00000000..de794340 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_instance_level/primary_role_with_factor_instances.rs @@ -0,0 +1,110 @@ +use crate::prelude::*; + +pub(crate) type PrimaryRoleWithFactorInstances = RoleWithFactorInstances<{ ROLE_PRIMARY }>; + +impl HasSampleValues for PrimaryRoleWithFactorInstances { + fn sample() -> Self { + MatrixOfFactorInstances::sample().primary_role + } + + fn sample_other() -> Self { + MatrixOfFactorInstances::sample_other().primary_role + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleWithFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + #[should_panic] + fn primary_role_non_securified_threshold_instances_is_err() { + let _ = SUT::with_factors( + 1, + [ + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_unsecurified_at_index(0).into() + ], + [] + ); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 2, + "thresholdFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "427969814e15d74c3ff4d9971465cb709d210c8a7627af9466bdaa67bd0929b7" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + }, + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ], + "overrideFactors": [] + } + "#, + ); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_instance_level/recovery_role_with_factor_instances.rs b/crates/rules/src/roles/factor_levels/factor_instance_level/recovery_role_with_factor_instances.rs new file mode 100644 index 00000000..3ce01203 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_instance_level/recovery_role_with_factor_instances.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; + +pub(crate) type RecoveryRoleWithFactorInstances = RoleWithFactorInstances<{ ROLE_RECOVERY }>; + +impl HasSampleValues for RecoveryRoleWithFactorInstances { + fn sample() -> Self { + MatrixOfFactorInstances::sample().recovery_role + } + + fn sample_other() -> Self { + MatrixOfFactorInstances::sample_other().recovery_role + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = RecoveryRoleWithFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_instance_level/role_with_factor_instances.rs b/crates/rules/src/roles/factor_levels/factor_instance_level/role_with_factor_instances.rs new file mode 100644 index 00000000..e8204238 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_instance_level/role_with_factor_instances.rs @@ -0,0 +1,77 @@ +use crate::prelude::*; + +pub(crate) type RoleWithFactorInstances = + AbstractBuiltRoleWithFactor; + +impl RoleWithFactorSources { + fn from(other: &RoleWithFactorSources) -> Self { + Self::with_factors( + other.get_threshold(), + other.get_threshold_factors().clone(), + other.get_override_factors().clone(), + ) + } +} + +impl MatrixOfFactorSources { + pub(crate) fn get_role(&self) -> RoleWithFactorSources { + match R { + ROLE_PRIMARY => RoleWithFactorSources::from(&self.primary_role), + ROLE_RECOVERY => RoleWithFactorSources::from(&self.recovery_role), + ROLE_CONFIRMATION => RoleWithFactorSources::from(&self.confirmation_role), + _ => panic!("unknown"), + } + } +} + +impl RoleWithFactorInstances { + pub(crate) fn fulfilling_role_of_factor_sources_with_factor_instances( + consuming_instances: &IndexMap, + matrix_of_factor_sources: &MatrixOfFactorSources, + ) -> Result { + let role_kind = RoleKind::from_u8(R).unwrap(); + + let role_of_sources = matrix_of_factor_sources.get_role::(); + assert_eq!(role_of_sources.role(), role_kind); + let threshold: u8 = role_of_sources.get_threshold(); + + // Threshold factors + let threshold_factors = + Self::try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( + consuming_instances, + role_of_sources.get_threshold_factors(), + )?; + + // Override factors + let override_factors = + Self::try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( + consuming_instances, + role_of_sources.get_override_factors(), + )?; + + let role_with_instances = + Self::with_factors(threshold, threshold_factors, override_factors); + + assert_eq!(role_with_instances.role(), role_kind); + Ok(role_with_instances) + } + + fn try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( + instances: &IndexMap, + from: &[FactorSource], + ) -> Result, CommonError> { + from.iter() + .map(|f| { + if let Some(existing) = instances.get(&f.id_from_hash()) { + let hd_instance = existing + .first() + .ok_or(CommonError::MissingFactorMappingInstancesIntoRole)?; + let instance = FactorInstance::from(hd_instance); + Ok(instance) + } else { + Err(CommonError::MissingFactorMappingInstancesIntoRole) + } + }) + .collect::, CommonError>>() + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_source_id_level/confirmation_role_with_factor_source_ids.rs b/crates/rules/src/roles/factor_levels/factor_source_id_level/confirmation_role_with_factor_source_ids.rs new file mode 100644 index 00000000..03c4d158 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_id_level/confirmation_role_with_factor_source_ids.rs @@ -0,0 +1,100 @@ +use crate::prelude::*; + +pub type ConfirmationRoleWithFactorSourceIds = RoleWithFactorSourceIds<{ ROLE_CONFIRMATION }>; + +impl HasSampleValues for ConfirmationRoleWithFactorSourceIds { + /// Config MFA 1.1 + fn sample() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_password()) + .unwrap(); + builder.build().unwrap() + } + + /// Config MFA 2.1 + fn sample_other() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_device()) + .unwrap(); + builder.build().unwrap() + } +} +impl HasSampleValues for RecoveryRoleWithFactorSourceIds { + /// Config MFA 1.1 + fn sample() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_device()) + .unwrap(); + + builder + .add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + builder.build().unwrap() + } + + /// Config MFA 3.3 + fn sample_other() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_ledger_other()) + .unwrap(); + + builder.build().unwrap() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = ConfirmationRoleWithFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn get_all_factors() { + let sut = SUT::sample(); + let factors = sut.all_factors(); + assert_eq!( + factors.len(), + sut.get_override_factors().len() + sut.get_threshold_factors().len() + ); + } + + #[test] + fn assert_json() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + } + ] + } + "#, + ); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_source_id_level/mod.rs b/crates/rules/src/roles/factor_levels/factor_source_id_level/mod.rs new file mode 100644 index 00000000..206017bd --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_id_level/mod.rs @@ -0,0 +1,9 @@ +mod confirmation_role_with_factor_source_ids; +mod primary_role_with_factor_source_ids; +mod recovery_role_with_factor_source_ids; +mod roles_with_factor_ids; + +pub use confirmation_role_with_factor_source_ids::*; +pub use primary_role_with_factor_source_ids::*; +pub use recovery_role_with_factor_source_ids::*; +pub use roles_with_factor_ids::*; diff --git a/crates/rules/src/roles/factor_levels/factor_source_id_level/primary_role_with_factor_source_ids.rs b/crates/rules/src/roles/factor_levels/factor_source_id_level/primary_role_with_factor_source_ids.rs new file mode 100644 index 00000000..d526ad79 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_id_level/primary_role_with_factor_source_ids.rs @@ -0,0 +1,104 @@ +use crate::prelude::*; + +pub type PrimaryRoleWithFactorSourceIds = RoleWithFactorSourceIds<{ ROLE_PRIMARY }>; + +impl PrimaryRoleWithFactorSourceIds { + /// Config MFA 1.1 + pub fn sample_primary() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + + builder + .add_factor_source_to_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(2).unwrap(); + builder.build().unwrap() + } +} + +impl HasSampleValues for PrimaryRoleWithFactorSourceIds { + fn sample() -> Self { + Self::sample_primary() + } + + fn sample_other() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + + builder + .add_factor_source_to_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(1).unwrap(); + builder.build().unwrap() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleWithFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn get_all_factors() { + let sut = SUT::sample_primary(); + let factors = sut.all_factors(); + assert_eq!( + factors.len(), + sut.get_override_factors().len() + sut.get_threshold_factors().len() + ); + } + + #[test] + fn get_threshold() { + let sut = SUT::sample_primary(); + assert_eq!(sut.get_threshold(), 2); + } + + #[test] + fn assert_json_sample_primary() { + let sut = SUT::sample_primary(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 2, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ], + "overrideFactors": [] + } + "#, + ); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_source_id_level/recovery_role_with_factor_source_ids.rs b/crates/rules/src/roles/factor_levels/factor_source_id_level/recovery_role_with_factor_source_ids.rs new file mode 100644 index 00000000..f0e7e111 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_id_level/recovery_role_with_factor_source_ids.rs @@ -0,0 +1,63 @@ +use crate::prelude::*; + +pub type RecoveryRoleWithFactorSourceIds = RoleWithFactorSourceIds<{ ROLE_RECOVERY }>; + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = RecoveryRoleWithFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn get_all_factors() { + let sut = SUT::sample(); + let factors = sut.all_factors(); + assert_eq!( + factors.len(), + sut.get_override_factors().len() + sut.get_threshold_factors().len() + ); + } + + #[test] + fn assert_json() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + } + "#, + ); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_source_id_level/roles_with_factor_ids.rs b/crates/rules/src/roles/factor_levels/factor_source_id_level/roles_with_factor_ids.rs new file mode 100644 index 00000000..815877b9 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_id_level/roles_with_factor_ids.rs @@ -0,0 +1,3 @@ +use crate::prelude::*; + +pub type RoleWithFactorSourceIds = AbstractBuiltRoleWithFactor; diff --git a/crates/rules/src/roles/factor_levels/factor_source_level/confirmation_role_with_factor_sources.rs b/crates/rules/src/roles/factor_levels/factor_source_level/confirmation_role_with_factor_sources.rs new file mode 100644 index 00000000..d324f8fe --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_level/confirmation_role_with_factor_sources.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; + +pub(crate) type ConfirmationRoleWithFactorSources = RoleWithFactorSources<{ ROLE_CONFIRMATION }>; + +impl HasSampleValues for ConfirmationRoleWithFactorSources { + fn sample() -> Self { + let ids = ConfirmationRoleWithFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } + + fn sample_other() -> Self { + let ids = ConfirmationRoleWithFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = ConfirmationRoleWithFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_source_level/mod.rs b/crates/rules/src/roles/factor_levels/factor_source_level/mod.rs new file mode 100644 index 00000000..b53a5e14 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_level/mod.rs @@ -0,0 +1,6 @@ +mod confirmation_role_with_factor_sources; +mod primary_role_with_factor_sources; +mod recovery_role_with_factor_sources; +mod roles_with_factor_sources; + +pub(crate) use roles_with_factor_sources::*; diff --git a/crates/rules/src/roles/factor_levels/factor_source_level/primary_role_with_factor_sources.rs b/crates/rules/src/roles/factor_levels/factor_source_level/primary_role_with_factor_sources.rs new file mode 100644 index 00000000..d4233277 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_level/primary_role_with_factor_sources.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; + +pub(crate) type PrimaryRoleWithFactorSources = RoleWithFactorSources<{ ROLE_PRIMARY }>; + +impl HasSampleValues for PrimaryRoleWithFactorSources { + fn sample() -> Self { + let ids = PrimaryRoleWithFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } + + fn sample_other() -> Self { + let ids = PrimaryRoleWithFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleWithFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_source_level/recovery_role_with_factor_sources.rs b/crates/rules/src/roles/factor_levels/factor_source_level/recovery_role_with_factor_sources.rs new file mode 100644 index 00000000..9c56d167 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_level/recovery_role_with_factor_sources.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; + +pub(crate) type RecoveryRoleWithFactorSources = RoleWithFactorSources<{ ROLE_RECOVERY }>; + +impl HasSampleValues for RecoveryRoleWithFactorSources { + fn sample() -> Self { + let ids = RecoveryRoleWithFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } + + fn sample_other() -> Self { + let ids = RecoveryRoleWithFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = RecoveryRoleWithFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/rules/src/roles/factor_levels/factor_source_level/roles_with_factor_sources.rs b/crates/rules/src/roles/factor_levels/factor_source_level/roles_with_factor_sources.rs new file mode 100644 index 00000000..20955fd9 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/factor_source_level/roles_with_factor_sources.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; + +pub(crate) type RoleWithFactorSources = AbstractBuiltRoleWithFactor; + +impl RoleWithFactorSources { + pub fn new( + role_with_factor_source_ids: RoleWithFactorSourceIds, + factor_sources: &FactorSources, + ) -> Result { + let lookup_f = |id: &FactorSourceID| -> Result { + factor_sources + .get_id(id) + .ok_or(CommonError::FactorSourceDiscrepancy) + .cloned() + }; + + let lookup = |ids: &Vec| -> Result, CommonError> { + ids.iter() + .map(lookup_f) + .collect::, CommonError>>() + }; + + let threshold_factors = lookup(role_with_factor_source_ids.get_threshold_factors())?; + let override_factors = lookup(role_with_factor_source_ids.get_override_factors())?; + + Ok(Self::with_factors( + role_with_factor_source_ids.get_threshold(), + threshold_factors, + override_factors, + )) + } +} diff --git a/crates/rules/src/roles/factor_levels/mod.rs b/crates/rules/src/roles/factor_levels/mod.rs new file mode 100644 index 00000000..4f5b7785 --- /dev/null +++ b/crates/rules/src/roles/factor_levels/mod.rs @@ -0,0 +1,7 @@ +mod factor_instance_level; +mod factor_source_id_level; +mod factor_source_level; + +pub use factor_instance_level::*; +pub use factor_source_id_level::*; +pub(crate) use factor_source_level::*; diff --git a/crates/rules/src/roles/mod.rs b/crates/rules/src/roles/mod.rs new file mode 100644 index 00000000..c99cb397 --- /dev/null +++ b/crates/rules/src/roles/mod.rs @@ -0,0 +1,7 @@ +mod abstract_role_builder_or_built; +mod builder; +mod factor_levels; + +pub(crate) use abstract_role_builder_or_built::*; +pub use builder::*; +pub use factor_levels::*; diff --git a/crates/rules/src/rules/error.rs b/crates/rules/src/rules/error.rs deleted file mode 100644 index a4a0a97c..00000000 --- a/crates/rules/src/rules/error.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::prelude::*; - - -#[derive(Clone, Copy, Debug, PartialEq, Eq, ThisError)] -pub enum Error { - #[error("Unknown")] - Unknown, -} \ No newline at end of file diff --git a/crates/rules/src/rules/mod.rs b/crates/rules/src/rules/mod.rs deleted file mode 100644 index 45d3692c..00000000 --- a/crates/rules/src/rules/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod error; - -pub use error::*; \ No newline at end of file diff --git a/crates/rules/src/security_structure_of_factors/abstract_security_structure_of_factors.rs b/crates/rules/src/security_structure_of_factors/abstract_security_structure_of_factors.rs new file mode 100644 index 00000000..615a23a2 --- /dev/null +++ b/crates/rules/src/security_structure_of_factors/abstract_security_structure_of_factors.rs @@ -0,0 +1,30 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbstractSecurityStructure { + /// Metadata of this Security Structure, such as globally unique and + /// stable identifier, creation date and user chosen label (name). + pub metadata: sargon::SecurityStructureMetadata, + + /// The structure of factors to use for certain roles, Primary, Recovery + /// and Confirmation role. + pub matrix_of_factors: AbstractMatrixBuilt, +} + +impl AbstractSecurityStructure { + pub fn with_metadata( + metadata: sargon::SecurityStructureMetadata, + matrix_of_factors: AbstractMatrixBuilt, + ) -> Self { + Self { + metadata, + matrix_of_factors, + } + } + + pub fn new(display_name: DisplayName, matrix_of_factors: AbstractMatrixBuilt) -> Self { + let metadata = sargon::SecurityStructureMetadata::new(display_name); + Self::with_metadata(metadata, matrix_of_factors) + } +} diff --git a/crates/rules/src/security_structure_of_factors/mod.rs b/crates/rules/src/security_structure_of_factors/mod.rs new file mode 100644 index 00000000..29db8460 --- /dev/null +++ b/crates/rules/src/security_structure_of_factors/mod.rs @@ -0,0 +1,7 @@ +mod abstract_security_structure_of_factors; +mod security_structure_of_factor_source_ids; +mod security_structure_of_factor_sources; + +pub(super) use abstract_security_structure_of_factors::*; +pub use security_structure_of_factor_source_ids::*; +pub use security_structure_of_factor_sources::*; diff --git a/crates/rules/src/security_structure_of_factors/security_structure_of_factor_source_ids.rs b/crates/rules/src/security_structure_of_factors/security_structure_of_factor_source_ids.rs new file mode 100644 index 00000000..404cbd9f --- /dev/null +++ b/crates/rules/src/security_structure_of_factors/security_structure_of_factor_source_ids.rs @@ -0,0 +1,167 @@ +use crate::prelude::*; + +pub type SecurityStructureOfFactorSourceIds = AbstractSecurityStructure; + +impl HasSampleValues for SecurityStructureOfFactorSourceIds { + fn sample() -> Self { + let metadata = sargon::SecurityStructureMetadata::sample(); + Self::with_metadata(metadata, MatrixOfFactorSourceIds::sample()) + } + + fn sample_other() -> Self { + let metadata = sargon::SecurityStructureMetadata::sample_other(); + Self::with_metadata(metadata, MatrixOfFactorSourceIds::sample_other()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityStructureOfFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "metadata": { + "id": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "displayName": "Spending Account", + "createdOn": "2023-09-11T16:05:56.000Z", + "lastUpdatedOn": "2023-09-11T16:05:56.000Z" + }, + "matrixOfFactors": { + "primaryRole": { + "threshold": 2, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + } + "#, + ); + } + + #[test] + fn assert_json_sample_other() { + let sut = SUT::sample_other(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "metadata": { + "id": "dededede-dede-dede-dede-dededededede", + "displayName": "Savings Account", + "createdOn": "2023-12-24T17:13:56.123Z", + "lastUpdatedOn": "2023-12-24T17:13:56.123Z" + }, + "matrixOfFactors": { + "primaryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + } + ] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "52ef052a0642a94279b296d6b3b17dedc035a7ae37b76c1d60f11f2725100077" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + } + "#, + ); + } +} diff --git a/crates/rules/src/security_structure_of_factors/security_structure_of_factor_sources.rs b/crates/rules/src/security_structure_of_factors/security_structure_of_factor_sources.rs new file mode 100644 index 00000000..205e3f9a --- /dev/null +++ b/crates/rules/src/security_structure_of_factors/security_structure_of_factor_sources.rs @@ -0,0 +1,33 @@ +use crate::prelude::*; + +pub type SecurityStructureOfFactorSources = AbstractSecurityStructure; + +impl HasSampleValues for SecurityStructureOfFactorSources { + fn sample() -> Self { + let metadata = sargon::SecurityStructureMetadata::sample(); + Self::with_metadata(metadata, MatrixOfFactorSources::sample()) + } + + fn sample_other() -> Self { + let metadata = sargon::SecurityStructureMetadata::sample_other(); + Self::with_metadata(metadata, MatrixOfFactorSources::sample_other()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityStructureOfFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 08f1265f..c4904ee8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly-2024-01-11" +channel = "nightly-2024-07-30"