From b96002e74d10a88054dd5e177f90c42e2fd19a6c Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Sat, 21 Sep 2024 08:01:10 +0700 Subject: [PATCH 01/12] feat(cat-gateway): Chain sync V2 - part 2 (#836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(docs): Fix up docs issues * fix(backend): Huge refactor to prep for scylladb config management * fix(backend): Clean up logging a little, and add build info logs as required for production. * Refactor and setup cassandra config/session * feat(backend): Index DB schema setup seems to work * WIP * fix(rust): Format fixes * fix(rust): Build fixes * fix(rust): Adjust index DB so we can index without querying, and can optimize on first detected spend. * fix(rust): add more docs * fix(rust): basic new follower integration * fix(rust): wip * fix(ci): Bump rust compiler version to match CI * ci(backend): Bump rust version to match CI * fix(backend): Fix code format and lints * feat(backend): simple new block indexer just to test the logic works * feat(gateway): Simple indexing with cassandra seems to work * refactor(backend): Remove lazy and once_cell in favor of new standard library replacements * fix(backend): WIP indexing for stake addresses and unstaked ada * fix(backend): indexing WIP * fix(backend): Add support for log control with env vars, default to mainnet, adjust `justfile` to properly select preprod and also refresh git dependencies. * feat(backend): Make local test scylla db run with 4 nodes, not 1 * fix(backend-lib): Add stop for cassandra db cluster * refactor(backend-lib): Remove c509-certificate because its moved to catalyst-libs * fix(backend): Remove dependencies from Workspace, and move into project * fix(backend): Use temporary cat-ci branch for rust builders * fix(backend): Remove obsolete common crates subdirectory * fix(backend): Don't use pre-packaged mithril snapshots in integration tests * fix(backend): Fix code so it builds with latest chain follower code. Also eliminates redundant logic now incorporated into chain follower. * fix(backend): Fix broken reference to catalyst libs * ci(ci): Bump all earthfiles to latest WIP cat-ci branch * fix(frontend-pkg): Ignore .dart_tool directory in frontend files checking markdown * fix(ci): Fix spelling * fix(spelling): Add more project words and properly sort list * fix(backend): Sync rust configs and add target to make it easier in future * fix(backend): Enable all features of Scylla for now. * fix(frontend-pkg): Fix markdown table having too many columns * ci(spelling): Fix spelling issues * fix(docs): Bump docs to latest WIP cat-ci version * feat(gateway): Add low resource scylla db instance for local testing * feat(gateway): Add and update developer convenience functions for backend * fix(backend): Fix code format * fix(backend): Fix spelling issues in CQL files * fix(spelling): Remove duplicates from the project words dictionary * fix(backend): Get the backend building properly with earthly. * feat(backend): remove obsoleted postgres logic for chain indexing * revert(event-db): Revert extension changes to sql files after fixing sqlfluff version * fix(frontend): Regenerate the dart api interface file, and add doing that to the pre-push just command * fix(backend): temporarily disable API tests * fix(backend): Also temporarily stop workflow consuming test reports that are disabled * fix(ci): Try and stop coveralls running for api-tests * ci(general): Replace temp CI branch with tagged release * feat: Add Handler for Permissionless Auth (#825) * docs(cips): Add Formal Defintion of auth token * fix(docs): Fix comments in cddl file * fix(docs): sig size * fix(docs): Rename CDDL for the auth token * docs(docs): Add auth-header documentation * docs(docs): Fix markdown line length error * docs(general): Fix spelling * fix(backend-lib): Bump to catalyst-libs tagged version * fix(backend): stub out obsolete code (to be removed in follow up PR). * fix(backend-lib): code format * fix(backend): remove unused crate dependencies * feat: auth token (#723) * feat(auth token encode and decode): permissionless auth * feat(auth token encode and decode): permissionless auth * feat(auth token encode and decode): permissionless auth * feat(auth token encode and decode): permissionless auth * feat(auth token encode and decode): permissionless auth * iron out tests * iron out tests * refactor(auth token encode and decode): ed25519 Signature cbor fields Sig over the preceding two fields - sig(cbor(kid), cbor(ulid)) * refactor(auth token encode and decode): ed25519 Signature cbor fields Sig over the preceding two fields - sig(cbor(kid), cbor(ulid)) * feat(cat security scheme): open api * feat(cat security scheme): open api * feat(mock cert state): given kid from bearer return pub key * feat(auth token): cache TTL * feat(auth token): cache TTL * feat(auth token): cache TT * ci(spell check): fix * ci(spell check): fix * ci(spell check): fix * refactor(clippy): housekeeping tidy * refactor(clippy): housekeeping tidy * refactor(clippy): housekeeping tidy * refactor(clippy): housekeeping tidy * fix(backend): Re-enable dependent crates used by this code * fix(backend): clippy lints * fix(backend): spelling --------- Co-authored-by: Steven Johnson Co-authored-by: Steven Johnson * feat: Update GET staked_ada endpoint to fetch from ScyllaDB (#728) * feat: get staked ada from scylladb * chore: revert justfile changes * chore: filter TXOs in rust instead of filtering in ScyllaDB query * fix(backend): spelling * fix(backend): Eliminate lint errors from Derived function * fix(backend): code format * fix(backend): Udate autogenerated dart code * chore(cat-voices): fix tests --------- Co-authored-by: Steven Johnson Co-authored-by: Steven Johnson Co-authored-by: Dominik Toton * feat: DB Indexing for CIP-36 registrations (#788) * feat: add schema for cip-36 registration tables * feat: index cip-36 by stake address * feat: index cip-36 registrations by vote key * fix: use TxiInserParams::new when adding txi data * fix: remove unused cfg attributes * fix: refactor Cip36RegistrationInsertQuery::new * fix(backend): Refactor queries and add multiple tables for cip36 registration indexes * fix(backend): Cip36 Primary key is stake key. Stake Key N->1 Vote Key * fix(backend): code format --------- Co-authored-by: Steven Johnson Co-authored-by: Steven Johnson * docs(general): Cleanup project dictionary * docs(spelling): Fix spelling * fix(backend): remove obsolete clippy lint cfg * docs(backend): Improve field documentation so its not ambiguous. * docs(backend): Fix comment * docs(backend): Improve comment * fix(backend): Vote Key index logic, and update comments * fix(backend): Earthfile needs to be executed from root of repo, to properly pick up secrets * fix(backend): make generic saturating value converter and use it instead of type specific ones * test(cat-gateway): Add tests for float conversion and better docs about functions limitations. * fix(cat-gateway): Developer lints in release mode, and also refer to correct local release binary * fix(cat-gateway): CIP36 index schema error * fix(cat-gateway): Cip36 indexing working, improve bad cassandra query reporting. --------- Co-authored-by: cong-or <60357579+cong-or@users.noreply.github.com> Co-authored-by: Felipe Rosa Co-authored-by: Dominik Toton Co-authored-by: Joaquín Rosales --- catalyst-gateway/Justfile | 16 +-- catalyst-gateway/bin/Cargo.toml | 2 +- .../bin/src/db/index/block/certs.rs | 38 +++++- .../db/index/block/cip36/cql/insert_cip36.cql | 4 +- .../cip36/cql/insert_cip36_for_vote_key.cql | 8 +- .../block/cip36/cql/insert_cip36_invalid.cql | 2 +- .../src/db/index/block/cip36/insert_cip36.rs | 22 +++- .../block/cip36/insert_cip36_for_vote_key.rs | 2 +- .../index/block/cip36/insert_cip36_invalid.rs | 24 +++- .../bin/src/db/index/block/cip36/mod.rs | 3 +- .../bin/src/db/index/block/mod.rs | 4 +- .../bin/src/db/index/block/txi.rs | 2 +- .../bin/src/db/index/block/txo/insert_txo.rs | 2 +- .../db/index/block/txo/insert_txo_asset.rs | 2 +- .../db/index/block/txo/insert_unstaked_txo.rs | 2 +- .../block/txo/insert_unstaked_txo_asset.rs | 2 +- .../bin/src/db/index/block/txo/mod.rs | 4 +- .../bin/src/db/index/queries/mod.rs | 16 ++- .../queries/staked_ada/update_txo_spent.rs | 2 +- .../index/schema/cql/cip36_registration.cql | 2 +- .../cql/cip36_registration_for_vote_key.cql | 2 +- .../schema/cql/cip36_registration_invalid.cql | 2 +- catalyst-gateway/bin/src/db/index/session.rs | 3 +- .../bin/src/service/utilities/convert.rs | 124 +++++++----------- 24 files changed, 169 insertions(+), 121 deletions(-) diff --git a/catalyst-gateway/Justfile b/catalyst-gateway/Justfile index e742d37910..a07d9b0c0e 100644 --- a/catalyst-gateway/Justfile +++ b/catalyst-gateway/Justfile @@ -22,19 +22,19 @@ code-format: # Lint the rust code code-lint: - cargo lintfix - cargo lint + cargo lintfix -r + cargo lint -r # Synchronize Rust Configs sync-cfg: - earthly +sync-cfg + cd .. && earthly ./catalyst-gateway+sync-cfg # Pre Push Checks pre-push: sync-cfg code-format code-lint license-check # Make sure we can actually build inside Earthly which needs to happen in CI. - earthly +check - earthly +build - earthly +package-cat-gateway + cd .. && earthly ./catalyst-gateway+check + cd .. && earthly ./catalyst-gateway+build + cd .. && earthly ./catalyst-gateway+package-cat-gateway # Build Local release build of catalyst gateway build-cat-gateway: code-format code-lint @@ -46,10 +46,10 @@ run-cat-gateway: build-cat-gateway CHAIN_FOLLOWER_SYNC_TASKS="16" \ RUST_LOG="error,cat-gateway=debug,cardano_chain_follower=debug,mithril-client=debug" \ CHAIN_NETWORK="Preprod" \ - ./catalyst-gateway/target/release/cat-gateway run --log-level debug + ./target/release/cat-gateway run --log-level debug # Run cat-gateway natively on mainnet run-cat-gateway-mainnet: build-cat-gateway CHAIN_FOLLOWER_SYNC_TASKS="1" \ RUST_LOG="error,cat-gateway=debug,cardano_chain_follower=debug,mithril-client=debug" \ - ./catalyst-gateway/target/release/cat-gateway run --log-level debug + ./target/release/cat-gateway run --log-level debug diff --git a/catalyst-gateway/bin/Cargo.toml b/catalyst-gateway/bin/Cargo.toml index 8d614a96b8..aed66bbb86 100644 --- a/catalyst-gateway/bin/Cargo.toml +++ b/catalyst-gateway/bin/Cargo.toml @@ -58,7 +58,7 @@ build-info = "0.0.38" ed25519-dalek = "2.1.1" scylla = { version = "0.14.0", features = ["cloud", "full-serialization"] } strum = { version = "0.26.3", features = ["derive"] } -# strum_macros = "0.26.4" +strum_macros = "0.26.4" openssl = { version = "0.10.66", features = ["vendored"] } num-bigint = "0.4.6" futures = "0.3.30" diff --git a/catalyst-gateway/bin/src/db/index/block/certs.rs b/catalyst-gateway/bin/src/db/index/block/certs.rs index 3c7ec9bcca..86240df12b 100644 --- a/catalyst-gateway/bin/src/db/index/block/certs.rs +++ b/catalyst-gateway/bin/src/db/index/block/certs.rs @@ -1,6 +1,6 @@ //! Index certs found in a transaction. -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; use cardano_chain_follower::MultiEraBlock; use pallas::ledger::primitives::{alonzo, conway}; @@ -12,7 +12,7 @@ use crate::{ queries::{FallibleQueryTasks, PreparedQueries, PreparedQuery, SizedBatch}, session::CassandraSession, }, - service::utilities::convert::u16_from_saturating, + service::utilities::convert::from_saturating, settings::CassandraEnvVars, }; @@ -37,6 +37,38 @@ pub(crate) struct StakeRegistrationInsertQuery { pool_delegation: MaybeUnset>, } +impl Debug for StakeRegistrationInsertQuery { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + let stake_address = match self.stake_address { + MaybeUnset::Unset => "UNSET", + MaybeUnset::Set(ref v) => &hex::encode(v), + }; + let register = match self.register { + MaybeUnset::Unset => "UNSET", + MaybeUnset::Set(v) => &format!("{v:?}"), + }; + let deregister = match self.deregister { + MaybeUnset::Unset => "UNSET", + MaybeUnset::Set(v) => &format!("{v:?}"), + }; + let pool_delegation = match self.pool_delegation { + MaybeUnset::Unset => "UNSET", + MaybeUnset::Set(ref v) => &hex::encode(v), + }; + + f.debug_struct("StakeRegistrationInsertQuery") + .field("stake_hash", &hex::encode(hex::encode(&self.stake_hash))) + .field("slot_no", &self.slot_no) + .field("txn", &self.txn) + .field("stake_address", &stake_address) + .field("script", &self.script) + .field("register", ®ister) + .field("deregister", &deregister) + .field("pool_delegation", &pool_delegation) + .finish() + } +} + /// TXI by Txn hash Index const INSERT_STAKE_REGISTRATION_QUERY: &str = include_str!("./cql/insert_stake_registration.cql"); @@ -130,7 +162,7 @@ impl CertInsertQuery { let (key_hash, pubkey, script) = match cred { pallas::ledger::primitives::conway::StakeCredential::AddrKeyhash(cred) => { let addr = block - .witness_for_tx(cred, u16_from_saturating(txn)) + .witness_for_tx(cred, from_saturating(txn)) .unwrap_or(default_addr); // Note: it is totally possible for the Registration Certificate to not be // witnessed. diff --git a/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36.cql b/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36.cql index 220954045c..1ecacb3493 100644 --- a/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36.cql +++ b/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36.cql @@ -8,7 +8,7 @@ INSERT INTO cip36_registration ( payment_address, is_payable, raw_nonce, - cip36, + cip36 ) VALUES ( :stake_address, :nonce, @@ -18,5 +18,5 @@ INSERT INTO cip36_registration ( :payment_address, :is_payable, :raw_nonce, - :cip36, + :cip36 ); diff --git a/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_for_vote_key.cql b/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_for_vote_key.cql index a09d36d3f5..b6d257f9c8 100644 --- a/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_for_vote_key.cql +++ b/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_for_vote_key.cql @@ -1,14 +1,14 @@ --- Index CIP-36 Registration (Valid) -INSERT INTO cip36_registration_for_stake_addr ( +-- Index CIP-36 Registration (For each Vote Key) +INSERT INTO cip36_registration_for_vote_key ( vote_key, stake_address, slot_no, txn, - valid, + valid ) VALUES ( :vote_key, :stake_address, :slot_no, :txn, - :valid, + :valid ); diff --git a/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_invalid.cql b/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_invalid.cql index 06162661fd..fac9b51d1a 100644 --- a/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_invalid.cql +++ b/catalyst-gateway/bin/src/db/index/block/cip36/cql/insert_cip36_invalid.cql @@ -10,7 +10,7 @@ INSERT INTO cip36_registration_invalid ( nonce, cip36, signed, - error_report, + error_report ) VALUES ( :stake_address, :slot_no, diff --git a/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36.rs b/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36.rs index d346124998..771cb9b5d2 100644 --- a/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36.rs +++ b/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36.rs @@ -1,6 +1,6 @@ //! Insert CIP36 Registration Query -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; use cardano_chain_follower::Metadata::cip36::{Cip36, VotingPubKey}; use scylla::{frame::value::MaybeUnset, SerializeRow, Session}; @@ -37,6 +37,26 @@ pub(super) struct Params { cip36: bool, } +impl Debug for Params { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let payment_address = match self.payment_address { + MaybeUnset::Unset => "UNSET", + MaybeUnset::Set(ref v) => &hex::encode(v), + }; + f.debug_struct("Params") + .field("stake_address", &self.stake_address) + .field("nonce", &self.nonce) + .field("slot_no", &self.slot_no) + .field("txn", &self.txn) + .field("vote_key", &self.vote_key) + .field("payment_address", &payment_address) + .field("is_payable", &self.is_payable) + .field("raw_nonce", &self.raw_nonce) + .field("cip36", &self.cip36) + .finish() + } +} + impl Params { /// Create a new Insert Query. pub fn new(vote_key: &VotingPubKey, slot_no: u64, txn: i16, cip36: &Cip36) -> Self { diff --git a/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_for_vote_key.rs b/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_for_vote_key.rs index 67a892d4f8..b7f0d48d83 100644 --- a/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_for_vote_key.rs +++ b/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_for_vote_key.rs @@ -16,7 +16,7 @@ const INSERT_CIP36_REGISTRATION_FOR_VOTE_KEY_QUERY: &str = include_str!("./cql/insert_cip36_for_vote_key.cql"); /// Insert CIP-36 Registration Invalid Query Parameters -#[derive(SerializeRow, Clone)] +#[derive(SerializeRow, Debug)] pub(super) struct Params { /// Voting Public Key vote_key: Vec, diff --git a/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_invalid.rs b/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_invalid.rs index 0ee5a4e5b1..0ab3fd8122 100644 --- a/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_invalid.rs +++ b/catalyst-gateway/bin/src/db/index/block/cip36/insert_cip36_invalid.rs @@ -1,6 +1,6 @@ //! Insert CIP36 Registration Query (Invalid Records) -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; use cardano_chain_follower::Metadata::cip36::{Cip36, VotingPubKey}; use scylla::{frame::value::MaybeUnset, SerializeRow, Session}; @@ -42,6 +42,28 @@ pub(super) struct Params { error_report: Vec, } +impl Debug for Params { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let cip36 = match self.cip36 { + MaybeUnset::Unset => "UNSET", + MaybeUnset::Set(v) => &format!("{v:?}"), + }; + f.debug_struct("Params") + .field("stake_address", &self.stake_address) + .field("slot_no", &self.slot_no) + .field("txn", &self.txn) + .field("vote_key", &self.vote_key) + .field("payment_address", &self.payment_address) + .field("is_payable", &self.is_payable) + .field("raw_nonce", &self.raw_nonce) + .field("nonce", &self.nonce) + .field("cip36", &cip36) + .field("signed", &self.signed) + .field("error_report", &self.error_report) + .finish() + } +} + impl Params { /// Create a new Insert Query. pub fn new( diff --git a/catalyst-gateway/bin/src/db/index/block/cip36/mod.rs b/catalyst-gateway/bin/src/db/index/block/cip36/mod.rs index aa7efe29b8..13d4c70b46 100644 --- a/catalyst-gateway/bin/src/db/index/block/cip36/mod.rs +++ b/catalyst-gateway/bin/src/db/index/block/cip36/mod.rs @@ -75,7 +75,8 @@ impl Cip36InsertQuery { vote_key, slot_no, txn_index, cip36, true, )); } - } else { + } else if cip36.stake_pk.is_some() { + // We can't index an error, if there is no stake public key. if cip36.voting_keys.is_empty() { self.invalid.push(insert_cip36_invalid::Params::new( None, diff --git a/catalyst-gateway/bin/src/db/index/block/mod.rs b/catalyst-gateway/bin/src/db/index/block/mod.rs index dd586ed6f8..775b55d502 100644 --- a/catalyst-gateway/bin/src/db/index/block/mod.rs +++ b/catalyst-gateway/bin/src/db/index/block/mod.rs @@ -14,7 +14,7 @@ use txi::TxiInsertQuery; use txo::TxoInsertQuery; use super::{queries::FallibleQueryTasks, session::CassandraSession}; -use crate::service::utilities::convert::i16_from_saturating; +use crate::service::utilities::convert::from_saturating; /// Add all data needed from the block into the indexes. pub(crate) async fn index_block(block: &MultiEraBlock) -> anyhow::Result<()> { @@ -34,7 +34,7 @@ pub(crate) async fn index_block(block: &MultiEraBlock) -> anyhow::Result<()> { // We add all transactions in the block to their respective index data sets. for (txn_index, txs) in block_data.txs().iter().enumerate() { - let txn = i16_from_saturating(txn_index); + let txn = from_saturating(txn_index); let txn_hash = txs.hash().to_vec(); diff --git a/catalyst-gateway/bin/src/db/index/block/txi.rs b/catalyst-gateway/bin/src/db/index/block/txi.rs index d3a37b3055..9dd4e0c8f9 100644 --- a/catalyst-gateway/bin/src/db/index/block/txi.rs +++ b/catalyst-gateway/bin/src/db/index/block/txi.rs @@ -14,7 +14,7 @@ use crate::{ }; /// Insert TXI Query and Parameters -#[derive(SerializeRow)] +#[derive(SerializeRow, Debug)] pub(crate) struct TxiInsertParams { /// Spent Transactions Hash txn_hash: Vec, diff --git a/catalyst-gateway/bin/src/db/index/block/txo/insert_txo.rs b/catalyst-gateway/bin/src/db/index/block/txo/insert_txo.rs index 7d9c0b6721..94837b5093 100644 --- a/catalyst-gateway/bin/src/db/index/block/txo/insert_txo.rs +++ b/catalyst-gateway/bin/src/db/index/block/txo/insert_txo.rs @@ -17,7 +17,7 @@ const INSERT_TXO_QUERY: &str = include_str!("./cql/insert_txo.cql"); /// Insert TXO Query Parameters /// (Superset of data to support both Staked and Unstaked TXO records.) -#[derive(SerializeRow)] +#[derive(SerializeRow, Debug)] pub(super) struct Params { /// Stake Address - Binary 28 bytes. 0 bytes = not staked. stake_address: Vec, diff --git a/catalyst-gateway/bin/src/db/index/block/txo/insert_txo_asset.rs b/catalyst-gateway/bin/src/db/index/block/txo/insert_txo_asset.rs index 9fa349237b..a42ea5b61e 100644 --- a/catalyst-gateway/bin/src/db/index/block/txo/insert_txo_asset.rs +++ b/catalyst-gateway/bin/src/db/index/block/txo/insert_txo_asset.rs @@ -15,7 +15,7 @@ const INSERT_TXO_ASSET_QUERY: &str = include_str!("./cql/insert_txo_asset.cql"); /// Insert TXO Asset Query Parameters /// (Superset of data to support both Staked and Unstaked TXO records.) -#[derive(SerializeRow)] +#[derive(SerializeRow, Debug)] pub(super) struct Params { /// Stake Address - Binary 28 bytes. 0 bytes = not staked. stake_address: Vec, diff --git a/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo.rs b/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo.rs index e27c7651c2..24957e92b3 100644 --- a/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo.rs +++ b/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo.rs @@ -14,7 +14,7 @@ const INSERT_UNSTAKED_TXO_QUERY: &str = include_str!("./cql/insert_unstaked_txo. /// Insert TXO Unstaked Query Parameters /// (Superset of data to support both Staked and Unstaked TXO records.) -#[derive(SerializeRow)] +#[derive(SerializeRow, Debug)] pub(super) struct Params { /// Transactions hash. txn_hash: Vec, diff --git a/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo_asset.rs b/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo_asset.rs index 8ac33aa129..78605f92ae 100644 --- a/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo_asset.rs +++ b/catalyst-gateway/bin/src/db/index/block/txo/insert_unstaked_txo_asset.rs @@ -15,7 +15,7 @@ const INSERT_UNSTAKED_TXO_ASSET_QUERY: &str = include_str!("./cql/insert_unstake /// Insert TXO Asset Query Parameters /// (Superset of data to support both Staked and Unstaked TXO records.) -#[derive(SerializeRow)] +#[derive(SerializeRow, Debug)] pub(super) struct Params { /// Transactions hash. txn_hash: Vec, diff --git a/catalyst-gateway/bin/src/db/index/block/txo/mod.rs b/catalyst-gateway/bin/src/db/index/block/txo/mod.rs index fc1ea2f306..9b4029fc3b 100644 --- a/catalyst-gateway/bin/src/db/index/block/txo/mod.rs +++ b/catalyst-gateway/bin/src/db/index/block/txo/mod.rs @@ -17,7 +17,7 @@ use crate::{ queries::{FallibleQueryTasks, PreparedQuery, SizedBatch}, session::CassandraSession, }, - service::utilities::convert::i16_from_saturating, + service::utilities::convert::from_saturating, settings::CassandraEnvVars, }; @@ -150,7 +150,7 @@ impl TxoInsertQuery { }; let staked = stake_address != NO_STAKE_ADDRESS; - let txo_index = i16_from_saturating(txo_index); + let txo_index = from_saturating(txo_index); if staked { let params = insert_txo::Params::new( diff --git a/catalyst-gateway/bin/src/db/index/queries/mod.rs b/catalyst-gateway/bin/src/db/index/queries/mod.rs index 89129f8d84..505918b58f 100644 --- a/catalyst-gateway/bin/src/db/index/queries/mod.rs +++ b/catalyst-gateway/bin/src/db/index/queries/mod.rs @@ -4,9 +4,9 @@ pub(crate) mod staked_ada; -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; -use anyhow::bail; +use anyhow::{bail, Context}; use crossbeam_skiplist::SkipMap; use scylla::{ batch::Batch, prepared_statement::PreparedStatement, serialize::row::SerializeRow, @@ -26,7 +26,8 @@ use crate::settings::{CassandraEnvVars, CASSANDRA_MIN_BATCH_SIZE}; pub(crate) type SizedBatch = SkipMap>; /// All Prepared Queries that we know about. -#[allow(clippy::enum_variant_names, dead_code)] +#[derive(strum_macros::Display)] +#[allow(clippy::enum_variant_names)] pub(crate) enum PreparedQuery { /// TXO Insert query. TxoAdaInsertQuery, @@ -206,7 +207,7 @@ impl PreparedQueries { /// /// This will divide the batch into optimal sized chunks and execute them until all /// values have been executed or the first error is encountered. - pub(crate) async fn execute_batch( + pub(crate) async fn execute_batch( &self, session: Arc, cfg: Arc, query: PreparedQuery, values: Vec, ) -> FallibleQueryResults { @@ -238,7 +239,12 @@ impl PreparedQueries { bail!("No batch query found for size {}", chunk_size); }; let batch_query_statements = batch_query.value().clone(); - results.push(session.batch(&batch_query_statements, chunk).await?); + results.push( + session + .batch(&batch_query_statements, chunk) + .await + .context(format!("query={query}, chunk={chunk:?}"))?, + ); } Ok(results) diff --git a/catalyst-gateway/bin/src/db/index/queries/staked_ada/update_txo_spent.rs b/catalyst-gateway/bin/src/db/index/queries/staked_ada/update_txo_spent.rs index 21658d74e2..0fe0a60bcf 100644 --- a/catalyst-gateway/bin/src/db/index/queries/staked_ada/update_txo_spent.rs +++ b/catalyst-gateway/bin/src/db/index/queries/staked_ada/update_txo_spent.rs @@ -17,7 +17,7 @@ use crate::{ const UPDATE_TXO_SPENT_QUERY: &str = include_str!("../cql/update_txo_spent.cql"); /// Update TXO spent query params. -#[derive(SerializeRow)] +#[derive(SerializeRow, Debug)] pub(crate) struct UpdateTxoSpentQueryParams { /// TXO stake address. pub stake_address: Vec, diff --git a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration.cql b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration.cql index 17c6886e3b..f9303e0e6d 100644 --- a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration.cql +++ b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration.cql @@ -15,4 +15,4 @@ CREATE TABLE IF NOT EXISTS cip36_registration ( PRIMARY KEY (stake_address, nonce, slot_no, txn) ) -WITH CLUSTERING ORDER BY (nonce, DESC, slot_no DESC, txn DESC); +WITH CLUSTERING ORDER BY (nonce DESC, slot_no DESC, txn DESC); diff --git a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql index 3ab03c8f1e..c3ba5f6dfc 100644 --- a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql +++ b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_for_vote_key.cql @@ -1,7 +1,7 @@ -- Index of CIP-36 registrations searchable by Stake Address. -- Full registration data needs to be queried from the man cip36 registration tables. -- Includes both Valid and Invalid registrations. -CREATE TABLE IF NOT EXISTS cip36_registration_for_stake_addr ( +CREATE TABLE IF NOT EXISTS cip36_registration_for_vote_key ( -- Primary Key Data vote_key blob, -- 32 Bytes of Vote Key. stake_address blob, -- 32 Bytes of Stake Address. diff --git a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_invalid.cql b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_invalid.cql index e72eaf304e..626b9d90ac 100644 --- a/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_invalid.cql +++ b/catalyst-gateway/bin/src/db/index/schema/cql/cip36_registration_invalid.cql @@ -15,6 +15,6 @@ CREATE TABLE IF NOT EXISTS cip36_registration_invalid ( signed boolean, -- Signature validates. error_report list, -- List of serialization errors in the registration. - PRIMARY KEY (vote_key, slot_no, txn) + PRIMARY KEY (stake_address, slot_no, txn) ) WITH CLUSTERING ORDER BY (slot_no DESC, txn DESC); diff --git a/catalyst-gateway/bin/src/db/index/session.rs b/catalyst-gateway/bin/src/db/index/session.rs index 884f5a7a7a..300bc9d92a 100644 --- a/catalyst-gateway/bin/src/db/index/session.rs +++ b/catalyst-gateway/bin/src/db/index/session.rs @@ -1,6 +1,7 @@ //! Session creation and storage use std::{ + fmt::Debug, path::PathBuf, sync::{Arc, OnceLock}, time::Duration, @@ -122,7 +123,7 @@ impl CassandraSession { /// /// This will divide the batch into optimal sized chunks and execute them until all /// values have been executed or the first error is encountered. - pub(crate) async fn execute_batch( + pub(crate) async fn execute_batch( &self, query: PreparedQuery, values: Vec, ) -> FallibleQueryResults { let session = self.session.clone(); diff --git a/catalyst-gateway/bin/src/service/utilities/convert.rs b/catalyst-gateway/bin/src/service/utilities/convert.rs index f5733f1360..04f5424dc5 100644 --- a/catalyst-gateway/bin/src/service/utilities/convert.rs +++ b/catalyst-gateway/bin/src/service/utilities/convert.rs @@ -1,94 +1,60 @@ //! Simple general purpose utility functions. -/// Convert T to an i16. (saturate if out of range.) -#[allow(dead_code)] // Its OK if we don't use this general utility function. -pub(crate) fn i16_from_saturating>(value: T) -> i16 { - match value.try_into() { - Ok(value) => value, - Err(_) => i16::MAX, - } -} - -/// Convert an `` to `u16`. (saturate if out of range.) -#[allow(dead_code)] // Its OK if we don't use this general utility function. -pub(crate) fn u16_from_saturating< +/// Convert an `` to ``. (saturate if out of range.) +/// Note can convert any int to float, or f32 to f64 as well. +/// can not convert from float to int, or f64 to f32. +pub(crate) fn from_saturating< + R: Copy + num_traits::identities::Zero + num_traits::Bounded, T: Copy - + TryInto + + TryInto + std::ops::Sub + std::cmp::PartialOrd + num_traits::identities::Zero, >( value: T, -) -> u16 { - if value < T::zero() { - u16::MIN - } else { - match value.try_into() { - Ok(value) => value, - Err(_) => u16::MAX, - } +) -> R { + match value.try_into() { + Ok(value) => value, + Err(_) => { + // If we couldn't convert, its out of range for the destination type. + if value > T::zero() { + // If the number is positive, its out of range in the positive direction. + R::max_value() + } else { + // Otherwise its out of range in the negative direction. + R::min_value() + } + }, } } -/// Convert an `` to `usize`. (saturate if out of range.) -#[allow(dead_code)] // Its OK if we don't use this general utility function. -pub(crate) fn usize_from_saturating< - T: Copy - + TryInto - + std::ops::Sub - + std::cmp::PartialOrd - + num_traits::identities::Zero, ->( - value: T, -) -> usize { - if value < T::zero() { - usize::MIN - } else { - match value.try_into() { - Ok(value) => value, - Err(_) => usize::MAX, - } - } -} +#[cfg(test)] +mod tests { -/// Convert an `` to `u32`. (saturate if out of range.) -#[allow(dead_code)] // Its OK if we don't use this general utility function. -pub(crate) fn u32_from_saturating< - T: Copy - + TryInto - + std::ops::Sub - + std::cmp::PartialOrd - + num_traits::identities::Zero, ->( - value: T, -) -> u32 { - if value < T::zero() { - u32::MIN - } else { - match value.try_into() { - Ok(converted) => converted, - Err(_) => u32::MAX, - } - } -} + use super::*; -/// Convert an `` to `u64`. (saturate if out of range.) -#[allow(dead_code)] // Its OK if we don't use this general utility function. -pub(crate) fn u64_from_saturating< - T: Copy - + TryInto - + std::ops::Sub - + std::cmp::PartialOrd - + num_traits::identities::Zero, ->( - value: T, -) -> u64 { - if value < T::zero() { - u64::MIN - } else { - match value.try_into() { - Ok(converted) => converted, - Err(_) => u64::MAX, - } + #[test] + #[allow(clippy::float_cmp)] + fn from_saturating_tests() { + let x: u32 = from_saturating(0_u8); + assert!(x == 0); + let x: u32 = from_saturating(255_u8); + assert!(x == 255); + let x: i8 = from_saturating(0_u32); + assert!(x == 0); + let x: i8 = from_saturating(512_u32); + assert!(x == 127); + let x: i8 = from_saturating(-512_i32); + assert!(x == -128); + let x: u16 = from_saturating(-512_i32); + assert!(x == 0); + let x: f64 = from_saturating(0.0_f32); + assert!(x == 0.0); + let x: f64 = from_saturating(0_u32); + assert!(x == 0.0); + let x: f64 = from_saturating(65536_u32); + assert!(x == 65536.0_f64); + let x: f64 = from_saturating(i32::MIN); + assert!(x == -2_147_483_648.0_f64); } } From 50348cd8d83ec0386d9805ea5ee0c7472c20b4e0 Mon Sep 17 00:00:00 2001 From: Dominik Toton <166132265+dtscalac@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:16:14 +0200 Subject: [PATCH 02/12] feat(cat-voices): password strength indicator (#861) * feat(cat-voices): password strength calculator * feat(cat-voices): add password strength indicator widget * chore(cat-voices): reformat --- .../voices_password_strength_indicator.dart | 116 ++++++++++++++++++ catalyst_voices/lib/widgets/widgets.dart | 1 + .../catalyst_voices_localizations.dart | 18 +++ .../catalyst_voices_localizations_en.dart | 9 ++ .../catalyst_voices_localizations_es.dart | 9 ++ .../lib/l10n/intl_en.arb | 12 ++ .../src/{ => auth}/authentication_status.dart | 0 .../lib/src/auth/password_strength.dart | 27 ++++ .../lib/src/catalyst_voices_models.dart | 3 +- .../catalyst_voices_models/pubspec.yaml | 4 +- .../test/auth/password_strength_test.dart | 55 +++++++++ .../examples/voices_indicators_example.dart | 52 +++++--- melos.yaml | 1 + 13 files changed, 285 insertions(+), 22 deletions(-) create mode 100644 catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart rename catalyst_voices/packages/catalyst_voices_models/lib/src/{ => auth}/authentication_status.dart (100%) create mode 100644 catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart create mode 100644 catalyst_voices/packages/catalyst_voices_models/test/auth/password_strength_test.dart diff --git a/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart b/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart new file mode 100644 index 0000000000..68333b325d --- /dev/null +++ b/catalyst_voices/lib/widgets/indicators/voices_password_strength_indicator.dart @@ -0,0 +1,116 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +/// An indicator for a [PasswordStrength]. +/// +/// Fills in all the available horizontal space, +/// use a [SizedBox] to limit it's width. +final class VoicesPasswordStrengthIndicator extends StatelessWidget { + final PasswordStrength passwordStrength; + + const VoicesPasswordStrengthIndicator({ + super.key, + required this.passwordStrength, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Label(passwordStrength: passwordStrength), + const SizedBox(height: 16), + _Indicator(passwordStrength: passwordStrength), + ], + ); + } +} + +class _Label extends StatelessWidget { + final PasswordStrength passwordStrength; + + const _Label({required this.passwordStrength}); + + @override + Widget build(BuildContext context) { + return Text( + switch (passwordStrength) { + PasswordStrength.weak => context.l10n.weakPasswordStrength, + PasswordStrength.normal => context.l10n.normalPasswordStrength, + PasswordStrength.strong => context.l10n.goodPasswordStrength, + }, + style: Theme.of(context).textTheme.bodySmall, + ); + } +} + +class _Indicator extends StatelessWidget { + static const double _backgroundTrackHeight = 4; + static const double _foregroundTrackHeight = 6; + static const double _tracksGap = 8; + + final PasswordStrength passwordStrength; + + const _Indicator({required this.passwordStrength}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: _foregroundTrackHeight, + child: LayoutBuilder( + builder: (context, constraints) { + final totalWidthOfAllGaps = + (PasswordStrength.values.length - 1) * _tracksGap; + final availableWidth = constraints.maxWidth - totalWidthOfAllGaps; + final trackWidth = availableWidth / PasswordStrength.values.length; + + return Stack( + children: [ + Positioned.fill( + top: 1, + child: Container( + height: _backgroundTrackHeight, + decoration: BoxDecoration( + color: Theme.of(context).colors.onSurfaceSecondary08, + borderRadius: BorderRadius.circular(_backgroundTrackHeight), + ), + ), + ), + for (final strength in PasswordStrength.values) + if (passwordStrength.index >= strength.index) + Positioned( + left: strength.index * (trackWidth + _tracksGap), + width: trackWidth, + child: _Track(passwordStrength: strength), + ), + ], + ); + }, + ), + ); + } +} + +class _Track extends StatelessWidget { + final PasswordStrength passwordStrength; + + const _Track({required this.passwordStrength}); + + @override + Widget build(BuildContext context) { + return Container( + height: _Indicator._foregroundTrackHeight, + decoration: BoxDecoration( + color: switch (passwordStrength) { + PasswordStrength.weak => Theme.of(context).colorScheme.error, + PasswordStrength.normal => Theme.of(context).colors.warning, + PasswordStrength.strong => Theme.of(context).colors.success, + }, + borderRadius: BorderRadius.circular(_Indicator._foregroundTrackHeight), + ), + ); + } +} diff --git a/catalyst_voices/lib/widgets/widgets.dart b/catalyst_voices/lib/widgets/widgets.dart index 0b9e550ea8..ff32170b6e 100644 --- a/catalyst_voices/lib/widgets/widgets.dart +++ b/catalyst_voices/lib/widgets/widgets.dart @@ -34,6 +34,7 @@ export 'indicators/process_progress_indicator.dart'; export 'indicators/voices_circular_progress_indicator.dart'; export 'indicators/voices_linear_progress_indicator.dart'; export 'indicators/voices_no_internet_connection_banner.dart'; +export 'indicators/voices_password_strength_indicator.dart'; export 'indicators/voices_status_indicator.dart'; export 'menu/voices_list_tile.dart'; export 'menu/voices_menu.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index 2ae03bd9b8..1105d7c665 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -645,6 +645,24 @@ abstract class VoicesLocalizations { /// In en, this message translates to: /// **'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'** String get noConnectionBannerDescription; + + /// Describes a password that is weak + /// + /// In en, this message translates to: + /// **'Weak password strength'** + String get weakPasswordStrength; + + /// Describes a password that has medium strength. + /// + /// In en, this message translates to: + /// **'Normal password strength'** + String get normalPasswordStrength; + + /// Describes a password that is strong. + /// + /// In en, this message translates to: + /// **'Good password strength'** + String get goodPasswordStrength; } class _VoicesLocalizationsDelegate extends LocalizationsDelegate { diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index 967640ee62..b4ae93ae36 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -336,4 +336,13 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get noConnectionBannerDescription => 'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'; + + @override + String get weakPasswordStrength => 'Weak password strength'; + + @override + String get normalPasswordStrength => 'Normal password strength'; + + @override + String get goodPasswordStrength => 'Good password strength'; } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index 33e8773d20..734ef69842 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -336,4 +336,13 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get noConnectionBannerDescription => 'Your internet is playing hide and seek. Check your internet connection, or try again in a moment.'; + + @override + String get weakPasswordStrength => 'Weak password strength'; + + @override + String get normalPasswordStrength => 'Normal password strength'; + + @override + String get goodPasswordStrength => 'Good password strength'; } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb index 215e697aaf..65c3750b5b 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -413,5 +413,17 @@ "noConnectionBannerDescription": "Your internet is playing hide and seek. Check your internet connection, or try again in a moment.", "@noConnectionBannerDescription": { "description": "Text shown in the No Internet Connection Banner widget for the description below the title." + }, + "weakPasswordStrength": "Weak password strength", + "@weakPasswordStrength": { + "description": "Describes a password that is weak" + }, + "normalPasswordStrength": "Normal password strength", + "@normalPasswordStrength": { + "description": "Describes a password that has medium strength." + }, + "goodPasswordStrength": "Good password strength", + "@goodPasswordStrength": { + "description": "Describes a password that is strong." } } \ No newline at end of file diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/authentication_status.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/authentication_status.dart similarity index 100% rename from catalyst_voices/packages/catalyst_voices_models/lib/src/authentication_status.dart rename to catalyst_voices/packages/catalyst_voices_models/lib/src/auth/authentication_status.dart diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart new file mode 100644 index 0000000000..5f5c1205b3 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/auth/password_strength.dart @@ -0,0 +1,27 @@ +import 'package:password_strength/password_strength.dart' as ps; + +/// Describes strength of a password. +/// +/// The enum values must be sorted from the weakest to the strongest. +enum PasswordStrength { + /// A weak password. Simple, already exposed, commonly used, etc. + weak, + + /// A medium password, not complex. + normal, + + /// A complex password with characters from different groups. + strong; + + /// The minimum length of accepted password. + static const int minimumPasswordLength = 8; + + factory PasswordStrength.calculate(String text) { + if (text.length < minimumPasswordLength) return PasswordStrength.weak; + + final strength = ps.estimatePasswordStrength(text); + if (strength <= 0.33) return PasswordStrength.weak; + if (strength <= 0.66) return PasswordStrength.normal; + return PasswordStrength.strong; + } +} diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart index 26b783cd49..072d1267fe 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -1,6 +1,7 @@ library catalyst_voices_models; -export 'authentication_status.dart'; +export 'auth/authentication_status.dart'; +export 'auth/password_strength.dart'; export 'errors/errors.dart'; export 'proposal/funded_proposal.dart'; export 'proposal/pending_proposal.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml index df456f50f1..a25c85724b 100644 --- a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml @@ -11,7 +11,9 @@ dependencies: equatable: ^2.0.5 flutter_quill: ^10.5.13 meta: ^1.10.0 + password_strength: ^0.2.0 dev_dependencies: catalyst_analysis: ^2.0.0 - test: ^1.24.9 + flutter_test: + sdk: flutter diff --git a/catalyst_voices/packages/catalyst_voices_models/test/auth/password_strength_test.dart b/catalyst_voices/packages/catalyst_voices_models/test/auth/password_strength_test.dart new file mode 100644 index 0000000000..108ef53ca9 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/test/auth/password_strength_test.dart @@ -0,0 +1,55 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(PasswordStrength, () { + test('weak password - too short', () { + expect( + PasswordStrength.calculate('123456'), + equals(PasswordStrength.weak), + ); + + expect( + PasswordStrength.calculate('Ab1!@_'), + equals(PasswordStrength.weak), + ); + }); + + test('weak password - too popular', () { + expect( + PasswordStrength.calculate('password'), + equals(PasswordStrength.weak), + ); + }); + + test('weak password - too simple', () { + expect( + /* cSpell:disable */ + PasswordStrength.calculate('simplepw'), + /* cSpell:enable */ + equals(PasswordStrength.weak), + ); + }); + + test('normal password', () { + expect( + PasswordStrength.calculate('Passwd12'), + equals(PasswordStrength.normal), + ); + }); + + test('strong password', () { + expect( + PasswordStrength.calculate('Passwd!@'), + equals(PasswordStrength.strong), + ); + }); + + test('strong password', () { + expect( + PasswordStrength.calculate('4Gf;Rd04WP,RxgBl)n5&RlG'), + equals(PasswordStrength.strong), + ); + }); + }); +} diff --git a/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart index 1f8e778b76..60c4b6c294 100644 --- a/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart +++ b/catalyst_voices/uikit_example/lib/examples/voices_indicators_example.dart @@ -1,5 +1,6 @@ import 'package:catalyst_voices/widgets/common/affix_decorator.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/material.dart'; @@ -16,9 +17,9 @@ class VoicesIndicatorsExample extends StatelessWidget { appBar: AppBar(title: const Text('Voices Indicators')), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 42, vertical: 24), - children: const [ - Text('Status Indicator'), - Row( + children: [ + const Text('Status Indicator'), + const Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -30,7 +31,7 @@ class VoicesIndicatorsExample extends StatelessWidget { ), title: Text('Your QR code verified successfully'), body: Text( - 'You can now use your QR-code 
to login into Catalyst.', + 'You can now use your QR-code to login into Catalyst.', ), type: VoicesStatusIndicatorType.success, ), @@ -45,24 +46,24 @@ class VoicesIndicatorsExample extends StatelessWidget { title: Text('Upload failed or QR code not recognized!'), body: Text( 'Are you sure your upload didn’t get interrupted or that ' - 'you provided 
a Catalyst QR code? ' - '

Please try again.', + 'you provided a Catalyst QR code? ' + 'Please try again.', ), type: VoicesStatusIndicatorType.error, ), ), ], ), - Text('Process Stepper Indicator'), - _Steps(), - Text('Linear - Indeterminate'), - VoicesLinearProgressIndicator(), - VoicesLinearProgressIndicator(showTrack: false), - Text('Linear - Fixed'), - VoicesLinearProgressIndicator(value: 0.25), - VoicesLinearProgressIndicator(value: 0.25, showTrack: false), - Text('Circular - Indeterminate'), - Row( + const Text('Process Stepper Indicator'), + const _Steps(), + const Text('Linear - Indeterminate'), + const VoicesLinearProgressIndicator(), + const VoicesLinearProgressIndicator(showTrack: false), + const Text('Linear - Fixed'), + const VoicesLinearProgressIndicator(value: 0.25), + const VoicesLinearProgressIndicator(value: 0.25, showTrack: false), + const Text('Circular - Indeterminate'), + const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ VoicesCircularProgressIndicator(), @@ -70,8 +71,8 @@ class VoicesIndicatorsExample extends StatelessWidget { VoicesCircularProgressIndicator(showTrack: false), ], ), - Text('Circular - Fixed'), - Row( + const Text('Circular - Fixed'), + const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ VoicesCircularProgressIndicator(value: 0.75), @@ -79,8 +80,19 @@ class VoicesIndicatorsExample extends StatelessWidget { VoicesCircularProgressIndicator(value: 0.75, showTrack: false), ], ), - Text('No Internet Connection Banner'), - NoInternetConnectionBanner(), + const Text('No Internet Connection Banner'), + const NoInternetConnectionBanner(), + const Text('Password strength indicator'), + for (final passwordStrength in PasswordStrength.values) + Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + child: VoicesPasswordStrengthIndicator( + passwordStrength: passwordStrength, + ), + ), + ), ].separatedByIndexed( (index, value) { return switch (value.runtimeType) { diff --git a/melos.yaml b/melos.yaml index 7378b9e202..2e38ced9f7 100644 --- a/melos.yaml +++ b/melos.yaml @@ -32,6 +32,7 @@ command: logging: ^1.2.0 meta: ^1.10.0 result_type: ^0.2.0 + password_strength: ^0.2.0 plugin_platform_interface: ^2.1.7 bech32: ^0.2.2 bip32_ed25519: ^0.6.0 From 177e2ee0433adf910bdc7165f745d409d102f8dd Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj <38898766+apskhem@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:30:57 +0700 Subject: [PATCH 03/12] feat(cat-voices): Add CIP-39 seed phrase utility (#852) * feat: bip39 * fix: constructor * refactor: single class * fix: docs * test: initial test * fix: typo * refactor: seed phrase class * test: seed phrase words * feat: full words * fix: cspell * refactor: move to models * refactor: rename test folder * test: add exceeding cases * refactor: test array * fix: linter for a file --- .../lib/src/seed_phrase.dart | 70 ++++++++++++++++++ .../catalyst_voices_models/pubspec.yaml | 2 + .../test/seed_phrase_test.dart | 74 +++++++++++++++++++ melos.yaml | 1 + 4 files changed, 147 insertions(+) create mode 100644 catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart create mode 100644 catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart new file mode 100644 index 0000000000..1b49fe942a --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart @@ -0,0 +1,70 @@ +// cspell: words wordlists WORDLIST +// ignore_for_file: implementation_imports + +import 'dart:typed_data'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip39/src/wordlists/english.dart'; +import 'package:convert/convert.dart'; + +/// Represents a seed phrase consisting of a mnemonic and provides methods for +/// generating and deriving cryptographic data from the mnemonic. +/// +/// The `SeedPhrase` class allows creation of a seed phrase either randomly, +/// from a given mnemonic, or from entropy data. It supports converting between +/// different formats, including Uint8List and hex strings. +class SeedPhrase { + /// The mnemonic phrase + final String mnemonic; + + /// Generates a new seed phrase with a random mnemonic. + /// + /// Throws an [ArgumentError] if the word count is invalid. + /// + /// [wordCount]: The number of words in the mnemonic. + /// The default word count is 12, but can specify 12, 15, 18, 21, or 24 words. + /// with a higher word count providing greater entropy and security. + SeedPhrase({int wordCount = 12}) + : this.fromMnemonic( + bip39.generateMnemonic( + strength: (wordCount * 32) ~/ 3, + ), + ); + + /// Creates a SeedPhrase from an existing [Uint8List] entropy. + /// + /// [encodedData]: The entropy data as a Uint8List. + SeedPhrase.fromUint8ListEntropy(Uint8List encodedData) + : this.fromHexEntropy(hex.encode(encodedData)); + + /// Creates a SeedPhrase from an existing hex-encoded entropy. + /// + /// [encodedData]: The entropy data as a hex string. + SeedPhrase.fromHexEntropy(String encodedData) + : this.fromMnemonic(bip39.entropyToMnemonic(encodedData)); + + /// Creates a SeedPhrase from an existing [mnemonic]. + /// + /// Throws an [ArgumentError] if the mnemonic is invalid. + /// + /// [mnemonic]: The mnemonic to derive the seed from. + SeedPhrase.fromMnemonic(this.mnemonic) + : assert(bip39.validateMnemonic(mnemonic), 'Invalid mnemonic phrase'); + + /// The seed derived from the mnemonic as a Uint8List. + Uint8List get uint8ListSeed => bip39.mnemonicToSeed(mnemonic); + + /// The seed derived from the mnemonic as a hex-encoded string. + String get hexSeed => bip39.mnemonicToSeedHex(mnemonic); + + /// The entropy derived from the mnemonic as a Uint8List. + Uint8List get uint8ListEntropy => Uint8List.fromList(hex.decode(hexEntropy)); + + /// The entropy derived from the mnemonic as a hex-encoded string. + String get hexEntropy => bip39.mnemonicToEntropy(mnemonic); + + /// The mnemonic phrase as a list of individual words. + List get mnemonicWords => mnemonic.split(' '); + + /// The full list of BIP-39 mnemonic words in English. + static List get wordList => WORDLIST; +} diff --git a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml index a25c85724b..0a336b0d57 100644 --- a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml @@ -7,7 +7,9 @@ environment: sdk: ">=3.5.0 <4.0.0" dependencies: + bip39: ^1.0.6 catalyst_cardano_serialization: ^0.4.0 + convert: ^3.1.1 equatable: ^2.0.5 flutter_quill: ^10.5.13 meta: ^1.10.0 diff --git a/catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart b/catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart new file mode 100644 index 0000000000..de075d6356 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart @@ -0,0 +1,74 @@ +import 'package:bip39/bip39.dart' as bip39; +import 'package:catalyst_voices_models/src/seed_phrase.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(SeedPhrase, () { + test('should generate a new SeedPhrase with random mnemonic', () { + final seedPhrase = SeedPhrase(); + expect(seedPhrase.mnemonic, isNotEmpty); + expect(seedPhrase.uint8ListSeed, isNotEmpty); + expect(seedPhrase.hexSeed, isNotEmpty); + expect(seedPhrase.mnemonicWords.length, 12); + }); + + test('should generate a seed phrase with 12, 15, 18, 21, and 24 words', () { + for (final wordCount in [12, 15, 18, 21, 24]) { + final seedPhrase = SeedPhrase(wordCount: wordCount); + expect(seedPhrase.mnemonicWords.length, wordCount); + expect(bip39.validateMnemonic(seedPhrase.mnemonic), isTrue); + } + }); + + test('should throw an error for an invalid word count', () { + expect(() => SeedPhrase(wordCount: 9), throwsA(isA())); + expect(() => SeedPhrase(wordCount: 13), throwsA(isA())); + expect(() => SeedPhrase(wordCount: 27), throwsA(isA())); + }); + + test('should create SeedPhrase from a valid mnemonic', () { + final validMnemonic = bip39.generateMnemonic(); + final seedPhrase = SeedPhrase.fromMnemonic(validMnemonic); + expect(seedPhrase.mnemonic, validMnemonic); + expect(seedPhrase.hexSeed, bip39.mnemonicToSeedHex(validMnemonic)); + }); + + test('should create SeedPhrase from hex-encoded entropy', () { + final entropy = bip39.mnemonicToEntropy(bip39.generateMnemonic()); + final seedPhrase = SeedPhrase.fromHexEntropy(entropy); + + expect(seedPhrase.mnemonic, isNotEmpty); + expect(seedPhrase.hexEntropy, entropy); + }); + + test('should throw an error for invalid mnemonic', () { + const invalidMnemonic = 'invalid mnemonic phrase'; + expect( + () => SeedPhrase.fromMnemonic(invalidMnemonic), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid mnemonic phrase'), + ), + ), + ); + }); + + test('should contain consistent mnemonic and seed in generated SeedPhrase', + () { + final seedPhrase = SeedPhrase(); + final mnemonic = seedPhrase.mnemonic; + final seed = seedPhrase.hexSeed; + + expect(bip39.mnemonicToSeedHex(mnemonic), seed); + }); + + test('should split mnemonic into a list of words', () { + final mnemonic = bip39.generateMnemonic(); + final seedPhrase = SeedPhrase.fromMnemonic(mnemonic); + final expectedWords = mnemonic.split(' '); + expect(seedPhrase.mnemonicWords, expectedWords); + }); + }); +} diff --git a/melos.yaml b/melos.yaml index 2e38ced9f7..345b88fae7 100644 --- a/melos.yaml +++ b/melos.yaml @@ -19,6 +19,7 @@ command: flutter: ">=3.24.1" dependencies: asn1lib: ^1.5.3 + bip39: ^1.0.6 bloc_concurrency: ^0.2.2 collection: ^1.18.0 cryptography: ^2.7.0 From b2a1ddec72257c9a299d00362805e383bb2121e1 Mon Sep 17 00:00:00 2001 From: Pal Dorogi <1113398+ilap@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:40:20 +0200 Subject: [PATCH 04/12] feat(dart/catalyst_cardano_serialization): add support for additional transaction body fields (#858) * feat(transaction_builder): add support for additional transaction body fields (#710) * fix(catalyst_cardano_serialization): add missing properties to Transaction constructor in `_buildBody()` * docs(catalyst_cardano_serialization): Correct typo in transaction builder class Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --------- Co-authored-by: Steven Johnson Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .../catalyst_cardano_serialization/README.md | 14 ++--- .../lib/src/builders/transaction_builder.dart | 55 ++++++++++++++++++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/README.md b/catalyst_voices_packages/catalyst_cardano_serialization/README.md index 7354c3ecce..4cb692b5d5 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/README.md +++ b/catalyst_voices_packages/catalyst_cardano_serialization/README.md @@ -194,15 +194,15 @@ Byron era addresses are not supported. | 5 = reward withdrawals | ❌️ | | 6 = protocol parameter update | ❌️ | | 7 = auxiliary_data_hash | ✔️ | -| 8 = validity interval start | ❌️ | -| 9 = mint | ❌️ | -| 11 = script_data_hash | ❌️ | -| 13 = collateral inputs | ❌️ | +| 8 = validity interval start | ✔️ | +| 9 = mint | ✔️ | +| 11 = script_data_hash | ✔️ | +| 13 = collateral inputs | ✔️ | | 14 = required signers | ✔️ | | 15 = network_id | ✔️ | -| 16 = collateral return | ❌️ | -| 17 = total collateral | ❌️ | -| 18 = reference inputs | ❌️ | +| 16 = collateral return | ✔️ | +| 17 = total collateral | ✔️ | +| 18 = reference inputs | ✔️ | ## Reference documentation diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart index b6470a4676..96d071ace6 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart @@ -38,13 +38,34 @@ final class TransactionBuilder extends Equatable { /// The transaction metadata as a list of key-value pairs (a map). final AuxiliaryData? auxiliaryData; - /// The list of public key hashes of addresses - /// that are required to sign the transaction. + /// Validity interval start as integer. + final SlotBigNum? validityStart; + + /// Mint as a non-zero uint64 multiasset. + final MultiAsset? mint; + + /// The transaction metadata as a list of key-value pairs (a map). + final ScriptData? scriptData; + + /// Collateral inputs as nonempty set. + final Set? collateralInputs; + + /// The list of public key hashes of addresses that are required to sign the + /// transaction. A nonempty set of addr key hashes. final Set? requiredSigners; /// Specifies on which network the code will run. final NetworkId? networkId; + /// Collateral return's transaction output. + final ShelleyMultiAssetTransactionOutput? collateralReturn; + + /// Total collateral as coin (uint64). + final Coin? totalCollateral; + + /// Reference inputs as nonempty set of transaction inputs. + final Set? referenceInputs; + /// The builder that builds the witness set of the transaction. /// /// The caller must know in advance how many witnesses there will be to @@ -60,8 +81,15 @@ final class TransactionBuilder extends Equatable { this.fee, this.ttl, this.auxiliaryData, + this.validityStart, + this.mint, + this.scriptData, + this.collateralInputs, this.requiredSigners, this.networkId, + this.collateralReturn, + this.totalCollateral, + this.referenceInputs, this.witnessBuilder = const TransactionWitnessSetBuilder( vkeys: {}, vkeysCount: 1, @@ -232,8 +260,15 @@ final class TransactionBuilder extends Equatable { fee, ttl, auxiliaryData, + validityStart, + mint, + scriptData, + collateralInputs, requiredSigners, networkId, + collateralReturn, + totalCollateral, + referenceInputs, witnessBuilder, ]; @@ -476,8 +511,17 @@ final class TransactionBuilder extends Equatable { auxiliaryDataHash: auxiliaryData != null ? AuxiliaryDataHash.fromAuxiliaryData(auxiliaryData!) : null, + validityStart: validityStart, + mint: mint, + scriptDataHash: scriptData != null + ? ScriptDataHash.fromScriptData(scriptData!) + : null, + collateralInputs: collateralInputs, requiredSigners: requiredSigners, networkId: networkId, + collateralReturn: collateralReturn, + totalCollateral: totalCollateral, + referenceInputs: referenceInputs, ); } @@ -493,8 +537,15 @@ final class TransactionBuilder extends Equatable { fee: fee ?? this.fee, ttl: ttl, auxiliaryData: auxiliaryData, + validityStart: validityStart, + mint: mint, + scriptData: scriptData, + collateralInputs: collateralInputs, requiredSigners: requiredSigners, networkId: networkId, + collateralReturn: collateralReturn, + totalCollateral: totalCollateral, + referenceInputs: referenceInputs, witnessBuilder: witnessBuilder ?? this.witnessBuilder, ); } From 1ce946514bc6d6702ce39debe873fb6f1d93f28e Mon Sep 17 00:00:00 2001 From: Alex Pozhylenkov Date: Mon, 23 Sep 2024 13:56:25 +0300 Subject: [PATCH 05/12] docs: Jormungandr voting crypto proofs spec. (#862) * add Non-Interactive ZK Tally Proof appendix * fix * fix markdown check * fix spelling * add Non-Interactive ZK Vote Proof Proover section and Verifier section * Finish verifier description * wip * fix spelling * fix --- .../08_concepts/voting_transaction/crypto.md | 320 +++++++++++++++--- 1 file changed, 276 insertions(+), 44 deletions(-) diff --git a/docs/src/architecture/08_concepts/voting_transaction/crypto.md b/docs/src/architecture/08_concepts/voting_transaction/crypto.md index f8dd29a8cf..5ae02eeaa1 100644 --- a/docs/src/architecture/08_concepts/voting_transaction/crypto.md +++ b/docs/src/architecture/08_concepts/voting_transaction/crypto.md @@ -28,7 +28,7 @@ and performing tally process for executing "Catalyst" fund events. ### Preliminaries -Through this paper we will use the following notations to refer to some entities of this protocol: +The protocol is based around the following entities: * **Proposal** - voting subject on which each voter will be cast their votes. @@ -49,13 +49,9 @@ Through this paper we will use the following notations to refer to some entities Or it could be defined based on their stake in the blockchain, which is more appropriate for web3 systems. -Important to note that current protocol defined to work with the one specific proposal, -so all definitions and procedures would be applied for some proposal. +Important to note that the protocol defined for some **one** proposal. Obviously, it could be easily scaled for a set of proposals, -performing all this protocol in parallel. - -The voting committee and voters registration/definition -are not subjects of this document. +performing protocol steps in parallel. ### Initial setup @@ -125,10 +121,10 @@ components would be defined as follows: -After the choice is done, -vote **must** be encrypted using shared shared election public key $pk$. +After the choice is done (described in [section](#voting-choice)), +vote **must** be encrypted using shared election public key $pk$. -Lifted ElGamal encryption algorithm is used, +To achieve that, Lifted ElGamal encryption algorithm is used, noted as $ElGamalEnc(message, randomness, public \; key)$. More detailed description of the lifted ElGamal algorithm you can find in the [appendix B](#b-lifted-elgamal-encryptiondecryption). @@ -138,17 +134,16 @@ $ElGamalEnc(message, randomness, public \; key)$ algorithm produces a ciphertext c = ElGamalEnc(message, randomness, public \; key) \end{equation} -To encode previously generated unit vector $\mathbf{e}_i$ ($i$ - voting choice identifier), -in more details you can read in this [section](#voting-choice), +To encrypt previously generated unit vector $\mathbf{e}_i$ ($i$ - voting choice identifier), for each vector component value $e_{i,j}$ generate a corresponding randomness.
Lets denote randomness value as $r_j$, -where $j$ states as the same identifier of the vector component $e_{i,j}$. +where $j$ is the same vector component's index $j$ value, $e_{i, j} => r_j$. -Then, for each vector component $e_{i,j}$ with the corresponding randomness, +Then, for each vector component $e_{i,j}$ with the corresponding randomness $r_j$, perform encryption algorithm applying shared election public key $pk$. \begin{equation} -c_j = Enc(e_{i,j}, r_j, pk) +c_j = ElGamalEnc(e_{i,j}, r_j, pk) \end{equation} As a result getting a vector $\mathbf{c}$ of ciphertext values $c_f$, @@ -156,10 +151,10 @@ with the size equals of the size $\mathbf{e}_t$ unit vector, equals to the amount of the voting options. Lets denote this vector as: \begin{equation} -\mathbf{c} = (c_1, \ldots, c_{M}) +\mathbf{c} = (c_1, \ldots, c_{M}) = (ElGamalEnc(e_{i,j}, r_j, pk), \ldots, ElGamalEnc(e_{i,M}, r_M, pk)) \end{equation} -where $M$ is the voting options amount. +where $M$ is the voting options amount and $i$ is the index of the voting choice. This is a first part of the published vote for a specific proposal. @@ -171,7 +166,7 @@ After the voter's choice is generated and encrypted, it is crucial to prove that [encoding](#voting-choice) and [encryption](#vote-encrypting) are formed correctly (i.e. that the voter indeed encrypt a unit vector). -Because by the definition of the encryption algorithm $Enc(message, randomness, public \; key)$ +Because by the definition of the encryption algorithm $ElGamalEnc(message, randomness, public \; key)$ it is possible to encrypt an any message value, it is not restricted for encryption only $0$ and $1$ values (as it was stated in the previous [section](#voting-choice), @@ -181,22 +176,28 @@ so everyone could validate a correctness of the encrypted vote data, without revealing a voting choice itself. To achieve that a some sophisticated ZK (Zero Knowledge) algorithm is used, -noted as $VotingChoiceProof(\mathbf{c})$. -It takes an encrypted vote vector $\mathbf{c}$ and generates a proof value $\pi$. +noted as $VoteProof(\mathbf{c}, \mathbf{e}_i, \mathbf{r}, pk)$. +It takes an encrypted vote vector $\mathbf{c}$, +an original vote unit vector $\mathbf{e}_i$, +a randomness vector $\mathbf{r}$, +which was used during encryption algorithm $ElGamalEnc$ +and an shared election public key $pk$. +As a result it generates a proof value $\pi$. \begin{equation} -\pi = VotingChoiceProof(\mathbf{c}) +\pi = VoteProof(\mathbf{c}, \mathbf{e}_i, \mathbf{r}, pk) \end{equation} -So to validate a $VotingChoiceCheck(\mathbf{c}, \pi)$ procedure should be used, -which takes an encrypted vote $\mathbf{c}$ and corresponded proof $\pi$ +So to validate a $VoteCheck(\mathbf{c}, \pi, pk)$ procedure should be used, +which takes an encrypted vote $\mathbf{c}$, corresponded proof $\pi$ +and the same hared election public key $pk$ as arguments and returns `true` or `false`, is it valid or not. \begin{equation} -true | false = VotingChoiceCheck(\mathbf{c}, \pi) +true | false = VoteCheck(\mathbf{c}, \pi, pk) \end{equation} -A more detailed description of how $VotingChoiceProof$, $VotingChoiceCheck$ work -you can find in the section *2.4* of this [paper][treasury_system_spec]. +A more detailed description of how $VoteProof$, $VoteCheck$ work +you can find in the [appendix D](#d-non-interactive-zk-vote-proof). #### Vote publishing @@ -305,7 +306,7 @@ Which proofs that a provided encrypted tally result value $er$ was decrypted int using the exact secret key $sk$, which is corresponded to the already known shared election public key $pk$. \begin{equation} -\pi = TallyProof(er, r, sk) +\pi = TallyProof(er, sk) \end{equation} So to validate a $TallyCheck(er, r, pk, \pi)$ procedure should be used, @@ -317,10 +318,8 @@ is it valid or not. true | false = TallyCheck(er, r, pk, \pi) \end{equation} - A more detailed description of how $TallyProof$, $TallyCheck$ work -you can find in the section *Fig. 10* of this [paper][treasury_system_spec]. - +you can find in the [appendix E](#e-non-interactive-zk-tally-proof). #### Tally publishing @@ -331,7 +330,7 @@ and tally proofs $\pi_i$ corresponded for each voting option of some proposal. It could be published using any public channel, e.g. blockchain, ipfs or through p2p network. -## A: Group definition +## A: Group Definition @@ -350,7 +349,7 @@ And defined as follows: -## B: Lifted ElGamal encryption/decryption +## B: Lifted ElGamal Encryption/Decryption @@ -363,12 +362,16 @@ More detailed how group operations are defined, described in [appendix A](#a-gro ### Encryption Lifted ElGamal encryption algorithm -takes as arguments $m$ message ($m \in \mathbb{Z}_q^*$), -$r$ randomness ($r \in \mathbb{Z}_q^*$), -$pk$ public key ($pk \in \mathbb{G}$): +takes as arguments: + +* $m$ - message ($m \in \mathbb{Z}_q$) +* $r$ - randomness ($r \in \mathbb{Z}_q$) +* $pk$ - public key ($pk \in \mathbb{G}$) + \begin{equation} ElGamalEnc(m, r, pk) = (c_1, c_2) = c, \end{equation} + \begin{equation} c_1 = g^r, \quad c_2 = g^m \circ pk^r \end{equation} @@ -377,8 +380,11 @@ $c$ - is a resulted ciphertext which consists of two elements $c_1, c_2 \in \mat ### Decryption -Lifted ElGamal decryption algorithm takes as arguments $c$ ciphertext, -$sk$ secret key ($sk \in \mathbb{Z}_q^*$): +Lifted ElGamal decryption algorithm takes as arguments: + +* $c$ - ciphertext, +* $sk$ - secret key ($sk \in \mathbb{Z}_q$) + \begin{equation} ElGamalDec(c, sk) = Dlog(c_2 \circ c_1^{-sk}) = m \end{equation} @@ -387,11 +393,11 @@ $m$ - an original message which was encrypted on the previous step, $Dlog(x)$ is a discrete logarithm of $x$. Note that since $Dlog$ is not efficient, the message space should be a small set, -say $m \in {{0,1}}^{\xi}$, for $\xi \le 30$. +say $m \in \{0,1\}^{\xi}$, for $\xi \le 30$. -## C: Homomorphic tally +## C: Homomorphic Tally @@ -399,11 +405,14 @@ Homomorphic tally schema is defined over any cyclic group $\mathbb{G}$ of order
More detailed how group operations are defined, described in [appendix A](#a-group-definition). -Homomorphic tally algorithm takes as arguments $i$ voting choice index, -$[\mathbf{c_1}, \mathbf{c_2}, \ldots, \mathbf{c_N}]$ -an array of encrypted votes vector's, -$[\alpha_1, \alpha_2, \ldots, \alpha_N]$ - an array of corresponded voter's voting power. -Where $N$ - votes amount. +Homomorphic tally algorithm takes as arguments: + +* $i$ - voting choice index +* $[\mathbf{c_1}, \mathbf{c_2}, \ldots, \mathbf{c_N}]$ - an array of encrypted votes vector's, + where $N$ - votes amount +* $[\alpha_1, \alpha_2, \ldots, \alpha_N]$ - an array of corresponded voter's voting power, + where $N$ - votes amount + \begin{equation} Tally(i, [\mathbf{c_1}, \mathbf{c_2}, \ldots, \mathbf{c_N}], [\alpha_1, \alpha_2, \ldots, \alpha_N]) = c_{1, i}^{\alpha_1} \circ c_{2, i}^{\alpha_2} \circ \ldots \circ c_{N, i}^{\alpha_N} = er_i @@ -419,6 +428,229 @@ it needs a decryption procedure corresponded for which encryption one was made. +## D: Non-Interactive ZK Vote Proof + +Non-Interactive ZK (Zero Knowledge) Vote Proof algorithm helps to solve only one problem, +to prove that the encrypted voting choice is exactly a some unit vector, +which consists of **only one** is $1$ value and others are $0$. + +A more detailed and formal description +you can find in the section *2.4* of this [paper][treasury_system_spec]. + +It is assumed that the original encryption and decryption is performing by ElGamal scheme. +It means that all described operations is also group dependent +(more about groups described in [appendix A](#a-group-definition)). + +### Prover + +The prover algorithm takes as arguments: + +* $\mathbf{c} = (c_0, \ldots, c_{M-1})$ - encrypted vote (a vector of ciphertext), + where $M$ is amount of voting options. +* $\mathbf{e}_i = (e_{i,0},\ldots, e_{i,M-1})$ - original voting choice, a unit vector, + where $M$ is amount of voting options + and $i$ is an index of the voting choice. +* $\mathbf{r} = (r_0, \ldots, r_{M-1})$ - a vector of randomnesses, + which was used during encryption. +* $pk$ - is a public key, which was used to encrypt a unit vector. + +So basically here is the relation between all these values: +\begin{equation} +\mathbf{c} = (c_1, \ldots, c_M) = (ElGamalEnc(e_{i,1}, r_1, pk), \ldots, ElGamalEnc(e_{i,M}, r_M, pk)) +\end{equation} + +\begin{equation} +VoteProof(\mathbf{c}, \mathbf{e}_i, \mathbf{r}, pk) = \pi +\end{equation} + +Important to note that the following notation would be used +$\{a_i\}$ - which is a set of some elements $a_i$. + +$\pi$ is the final proof. +To compute it, prover needs to perform the next steps: + +1. If the number of voting options $M$ is not a perfect power of $2$, + extend the vector $\mathbf{c}$ with $c_j = ElGamalEnc(0, 0, pk)$, + where $N$ is a perfect power of $2$, $j \in [M, \ldots, N - 1]$. + So the resulted $\mathbf{c} = (c_1, \ldots, c_M, \{c_j\})$. +2. Generate a commitment key $ck \in \mathbb{G}$. +3. Let $i_k$ is a bit value of the $i$-th binary representation, + where $k \in [0, log_2(N) - 1]$. + E.g. $i=3$ and $N=8, log_2(N) = 3$, + its binary representation $i=011$, + $i_0=0, i_1=1, i_2=1$. +4. For $l \in [0, \ldots, log_2(N)-1]$ generate a random values + $\alpha_l, \beta_l, \gamma_l, \delta_l, \in \mathbb{Z}_q$. +5. For $l \in [0, \ldots, log_2(N)-1]$ calculate, where $g$ is the group generator: + * $I_l = g^{i_l} \circ ck^{\alpha_l}, I_l \in \mathbb{G}$. + * $B_l = g^{\beta_l} \circ ck^{\gamma_l}, B_l \in \mathbb{G}$. + * $A_l = g^{i_l * \beta_l} \circ ck^{\delta_l}, A_l \in \mathbb{G}$. +6. Calculate a first verifier challenge + $com_1 = H(ck, pk, \{c_j\}, \{I_l\}, \{B_l\}, \{A_l\})$, + where $H$ is a hash function, + $j \in [0, \ldots, N-1]$ + and $l \in [0, \ldots, log_2(N)-1]$. +7. For $j \in [0, \ldots, N-1]$ calculate polynomials + in the following form $p_j(x) = e_{i, j}*x^{log_2(N)} + \sum_{l=0}^{log_2(N)-1} p_{j,l} * x^l$: + * $j_l$ is a bit value of the $j$-th binary representation (same as was described in step `3`). + * $z_l^{1} = i_l * x + \beta_l$. + * $z_l^{0} = x - z_l^{1} = (1 - i_l)*x - \beta_l$. + * Calculate the polynomial itself $p_j(x) = \prod_{l=0}^{log_2(N)-1} z_l^{j_l}$ +8. For $l \in [0, \ldots, log_2(N)-1]$ generate a random $R_l \in \mathbb{Z}_q$. +9. For $l \in [0, \ldots, log_2(N)-1]$ compute + $D_l = ElGamalEnc(sum_l, R_l, pk)$, + where $sum_l = \sum_{j=0}^{N-1}(p_{j,l} * com_1^j)$ + and $p_{j,l}$ - corresponding coefficients of the polynomial $p_j(x)$ calculated on step `7`. +10. Calculate a second verifier challenge + $com_2 = H(com_1, \{D_l\})$, + where $H$ is a hash function + and $l \in [0, \ldots, log_2(N)-1]$. +11. For $l \in [0, \ldots, log_2(N)-1]$ calculate: + * $z_l = i_l * com_2 + \beta_l, z_l \in \mathbb{Z}_q$. + * $w_l = \alpha_l * com_2 + \gamma_l, w_l \in \mathbb{Z}_q$. + * $v_l = \alpha_l * (com_2 - z_l) + \delta_l, v_l \in \mathbb{Z}_q$. +12. Calculate + $R=\sum_{j=0}^{N-1}(r_j * (com_2)^{log_2(N)} * (com_1)^j) + \sum_{l=0}^{log_2(N)-1}(R_l * (com_2)^l)$, + where $r_j$ original random values which was used to encrypt $c_j$ + and $R_l$ random values generated in step `8`. + +Finally, the proof is $\pi = (ck, \{I_l\}, \{B_l\}, \{A_l\}, \{D_l\}, \{z_l\}, \{w_l\}, \{v_l\}, R)$, +where $l \in [0, \ldots, log_2(N)-1]$. + +### Verifier + +The verifier algorithm takes as arguments: + +* $\mathbf{c} = (c_0, \ldots, c_{M-1})$ - encrypted vote (a vector of ciphertext), + where $M$ is amount of voting options. +* $\pi$ - a prover's proof generated on the [previous step](#prover) +* $pk$ - is a public key, which was used to encrypt a unit vector. + +\begin{equation} +VoteCheck(\mathbf{c}, \pi, pk) = true | false +\end{equation} + +As a result algorithm will return `true` or `false`, +is the verification was succeeded or not respectively. + +Knowing that $\pi$ equals to $(ck, \{I_l\}, \{B_l\}, \{A_l\}, \{D_l\}, \{z_l\}, \{w_l\}, \{v_l\}, R)$, +verifier needs to perform the next steps: + +1. If the number of voting options $M$ is not a perfect power of $2$, + extend the vector $\mathbf{c}$ with $c_j = ElGamalEnc(0, 0, pk)$, + where $N$ is a perfect power of $2$, $j \in [M, \ldots, N - 1]$. + So the resulted $\mathbf{c} = (c_1, \ldots, c_M, \{c_j\})$. +2. Calculate the first verifier challenge + $com_1 = H(ck, pk, \{c_j\}, \{I_l\}, \{B_l\}, \{A_l\})$, + where $H$ is a hash function, + $j \in [0, \ldots, N-1]$ + and $l \in [0, \ldots, log_2(N)-1]$. +3. Calculate a second verifier challenge + $com_2 = H(com_1, \{D_l\})$, + where $H$ is a hash function + and $l \in [0, \ldots, log_2(N)-1]$. +4. For $l \in [0, \ldots, log_2(N)-1]$ verify that the following statements are `true`, + where $g$ is the group generator: + * $(I_l)^{com_2} \circ B_l == g^{z_l} \circ ck^{w_l}$. + * $(I_l)^{com_2 - z_l} \circ A_l == g^{0} \circ ck^{v_l}$. +5. Calculate the following $Left = ElGamalEnc(0, R, pk)$. + Note that the $Left$ is a ciphertext, $Left = (Left_1, Left_2)$. +6. Note that $D_l$ is a ciphertext, + $D_l = (D_{l,1}, D_{l,2})$, for $l \in [0, \ldots, log_2(N)-1]$ + calculate the following: + * $Right2_1 = (D_{0,1})^{0} \circ \ldots \circ (D_{log_2(N) - 1,1})^{log_2(N) - 1}$. + * $Right2_2 = (D_{0,2})^{0} \circ \ldots \circ (D_{log_2(N) - 1,2})^{log_2(N) - 1}$. +7. For $j \in [0, \ldots, N-1]$ calculate the $p_j(com_2)$, + where $p_j$ is a prover's defined polynomial defined in step `7`: + * $j_l$ is a bit value of the $j$-th binary representation. + * $z_l^1 = z_j$. + * $z_l^0 = com_2 - z_j^1$. + * $p_j(com_2) = \prod_l^{log_2(N)-1} z_l^{j_l}$. +8. For $j \in [0, \ldots, N-1]$ calculate the $P_j = ElGamalEnc(-p_j(com_2), 0, pk)$. + Note that the $P_j$ is a ciphertext, $P_j = (P_{j,1}, P_{j,2})$. +9. Note that $C_j$ is a ciphertext, + $C_j = (C_{j,1}, C_{j,2})$, for $j \in [0, \ldots, N-1]$ + calculate: + * $Right1_{j,1} = (C_{j,1})^{com_2^{log_2(N)}} \circ (P_{j,1})^{com_1^{j}}$. + * $Right1_{j,2} = (C_{j,2})^{com_2^{log_2(N)}} \circ (P_{j,2})^{com_1^{j}}$. + * $Right1_{1} = Right1_{j,1} \circ \ldots \circ Right1_{N - 1, 1}$. + * $Right1_{2} = Right1_{j,2} \circ \ldots \circ Right1_{N - 1, 2}$. +10. Verify that the following statements are `true`: + * $Right1_{1} \circ Right2_1 == Left_1$. + * $Right1_{2} \circ Right2_2 == Left_2$. + +If step `4` and `10` returns `true` so the final result is `true` otherwise return `false`. + +## E: Non-Interactive ZK Tally Proof + +Non-Interactive ZK (Zero Knowledge) Tally Proof algorithm helps to solve only one problem, +to prove that the specific encrypted message was decrypted into the specific resulted value, +using exactly that secret key, +which is corresponds to the some shared public key. + + +A more detailed and formal description +you can find in the sections *Fig. 10* and *2.1.5* of this [paper][treasury_system_spec]. + + +It is assumed that the original encryption and decryption is performing by ElGamal scheme. +It means that all described operations is also group dependent +(more about groups described in [appendix A](#a-group-definition)). + +### Prover + +The prover algorithm takes as arguments: + +* $enc$ - an encrypted message (ciphertext). +* $sk$ - a secret key which was used to decrypt a message $enc$. + +\begin{equation} +TallyProof(enc, sk) = \pi +\end{equation} + +$\pi$ is the final proof. +To compute it, prover needs to perform the next steps: + +1. Take the first element of the ciphertext $enc = (enc_1, enc_2)$ + and calculate $d = enc_1^{sk}$. +2. Generate a random value $\mu, \quad \mu \in \mathbb{Z}_q$. +3. Compute $A_1 = g^{\mu}$, where $g$ is the group generator ($A_1 \in \mathbb{G}$). +4. Compute $A_2 = (enc_1)^{\mu}, \quad A_2 \in \mathbb{G}$. +5. Compute $e = H(pk, d, g, enc_1, A_1, A_2 )$, + where $pk$ is a corresponding public key of $sk$, $H$ is a hash function. +6. Compute $z = sk * e + \mu, \quad z \in \mathbb{Z}_q$. + +Finally, the proof is $\pi = (A_1, A_2, z)$. + +### Verifier + +The verifier algorithm takes as arguments: + +* $enc$ - an encrypted message (ciphertext). +* $dec$ - a decrypted message from the encrypted ciphertext $enc$. +* $pk$ - a public key corresponded to the $sk$ + which was supposedly used to decrypt a message $enc$. +* $\pi$ - a prover's proof generated on the [previous step](#prover-1). + +\begin{equation} +TallyCheck(enc, dec, pk, \pi) = true | false +\end{equation} + +As a result algorithm will return `true` or `false`, +is the verification was succeeded or not respectively. + +Knowing that $\pi$ equals to $(A_1, A_2, z)$, +verifier needs to perform the next steps: + +1. Take the first and second elements $enc_1, enc_2$ + of the ciphertext $enc = (enc_1, enc_2)$. +2. Calculate $d = g^{dec} \circ (-enc_2), \quad d \in \mathbb{G}$. +3. Compute $e = H(pk, d, g, enc_1, A_1, A_2 )$, where $g$ is the group generator. +4. Verify $g^z == pk^e \circ A_1$. +5. Verify $enc_1^z == d^e \circ A_2$. + +If step `3` and `4` returns `true` so the final result is `true` otherwise return `false`. + ## Rationale ## Path to Active From 24f86587423ab1411e9322a5d29978e5259480bd Mon Sep 17 00:00:00 2001 From: digitalheartxs Date: Mon, 23 Sep 2024 13:25:34 +0200 Subject: [PATCH 06/12] feat(cat-voices): Account popup (#857) * feat: My account, develop account_popup.dart * feat: My account, account_popup, pass callbacks * feat: My account, account_popup, ignore pointer for avatar * feat: My account, account_popup, lint issues * feat: My account, account_popup, lint issues * feat: My account, account_popup, use enum * feat: My account, account_popup, fields above constructor * feat: My account, account_popup, use VoicesDivider --------- Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .config/dictionaries/project.dic | 1 + .../lib/pages/account/account_popup.dart | 253 ++++++++++++++++++ .../app_bar/session/session_state_header.dart | 23 +- 3 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 catalyst_voices/lib/pages/account/account_popup.dart diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 2a20967c22..52715d4bb0 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -1,6 +1,7 @@ aapt aarch abnf +addr addrr adminer afinet diff --git a/catalyst_voices/lib/pages/account/account_popup.dart b/catalyst_voices/lib/pages/account/account_popup.dart new file mode 100644 index 0000000000..44f91a83e2 --- /dev/null +++ b/catalyst_voices/lib/pages/account/account_popup.dart @@ -0,0 +1,253 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class AccountPopup extends StatelessWidget { + final String avatarLetter; + final VoidCallback? onProfileKeychainTap; + final VoidCallback? onLockAccountTap; + + const AccountPopup({ + super.key, + required this.avatarLetter, + this.onProfileKeychainTap, + this.onLockAccountTap, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton<_MenuItemValue>( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, + onSelected: (_MenuItemValue value) { + switch (value) { + case _MenuItemValue.profileAndKeychain: + onProfileKeychainTap?.call(); + break; + case _MenuItemValue.lock: + onLockAccountTap?.call(); + break; + } + }, + itemBuilder: (BuildContext bc) { + return [ + PopupMenuItem( + padding: EdgeInsets.zero, + enabled: false, + value: null, + child: _Header( + accountLetter: avatarLetter, + walletName: 'Wallet name', + walletBalance: '₳ 1,750,000', + accountType: 'Basis', + walletAddress: 'addr1_H4543...45GH', + ), + ), + const PopupMenuItem( + height: 48, + padding: EdgeInsets.zero, + enabled: false, + value: null, + child: _Section('My account'), + ), + PopupMenuItem( + padding: EdgeInsets.zero, + value: _MenuItemValue.profileAndKeychain, + child: _MenuItem( + 'Profile & Keychain', + VoicesAssets.icons.userCircle, + ), + ), + PopupMenuItem( + padding: EdgeInsets.zero, + value: _MenuItemValue.lock, + child: _MenuItem( + 'Lock account', + VoicesAssets.icons.lockClosed, + showDivider: false, + ), + ), + ]; + }, + offset: const Offset(0, kToolbarHeight), + child: IgnorePointer( + child: VoicesAvatar( + icon: Text(avatarLetter), + ), + ), + ); + } +} + +class _Header extends StatelessWidget { + final String accountLetter; + final String walletName; + final String walletBalance; + final String accountType; + final String walletAddress; + + const _Header({ + required this.accountLetter, + required this.walletName, + required this.walletBalance, + required this.accountType, + required this.walletAddress, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: _padding), + child: Row( + children: [ + VoicesAvatar( + icon: Text(accountLetter), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(_padding), + child: Wrap( + children: [ + Text( + walletName, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + walletBalance, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ), + VoicesChip.rectangular( + content: Text( + accountType, + style: TextStyle( + color: Theme.of(context).colors.successContainer, + ), + ), + backgroundColor: Theme.of(context).colors.success, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: _padding, + right: _padding, + bottom: _padding, + top: 8, + ), + child: Row( + children: [ + Expanded( + child: Text( + walletAddress, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + InkWell( + onTap: () async { + await Clipboard.setData( + ClipboardData(text: walletAddress), + ); + }, + child: VoicesAssets.icons.clipboardCopy.buildIcon(), + ), + ], + ), + ), + VoicesDivider( + height: 1, + color: Theme.of(context).colors.outlineBorder, + indent: 0, + endIndent: 0, + ), + ], + ); + } +} + +class _MenuItem extends StatelessWidget { + final String text; + final SvgGenImage icon; + final bool showDivider; + + const _MenuItem( + this.text, + this.icon, { + this.showDivider = true, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 47, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: _padding), + child: Row( + children: [ + icon.buildIcon(), + const SizedBox(width: _padding), + Text( + text, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + if (showDivider) + VoicesDivider( + height: 1, + color: Theme.of(context).colors.outlineBorderVariant, + indent: 0, + endIndent: 0, + ), + ], + ); + } +} + +class _Section extends StatelessWidget { + final String text; + + const _Section( + this.text, + ); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 47, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: _padding), + child: Text( + text, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + VoicesDivider( + height: 1, + color: Theme.of(context).colors.outlineBorderVariant, + indent: 0, + endIndent: 0, + ), + ], + ); + } +} + +const _padding = 12.0; + +enum _MenuItemValue { + profileAndKeychain, + lock, +} diff --git a/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart b/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart index 8f110ec863..a84223a205 100644 --- a/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart +++ b/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart @@ -1,3 +1,4 @@ +import 'package:catalyst_voices/pages/account/account_popup.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; @@ -15,8 +16,11 @@ class SessionStateHeader extends StatelessWidget { return switch (state) { VisitorSessionState() => const _VisitorButton(), GuestSessionState() => const _GuestButton(), - ActiveUserSessionState(:final user) => - _ActiveUserAvatar(letter: user.acronym ?? 'A'), + ActiveUserSessionState(:final user) => AccountPopup( + avatarLetter: user.acronym ?? 'A', + onLockAccountTap: () => debugPrint('Lock account'), + onProfileKeychainTap: () => debugPrint('Open Profile screen'), + ), }; }, ); @@ -46,18 +50,3 @@ class _VisitorButton extends StatelessWidget { ); } } - -class _ActiveUserAvatar extends StatelessWidget { - final String letter; - - const _ActiveUserAvatar({ - required this.letter, - }); - - @override - Widget build(BuildContext context) { - return VoicesAvatar( - icon: Text(letter), - ); - } -} From 53f122c376a739eb1abd1766bbfbd616c4b3ee63 Mon Sep 17 00:00:00 2001 From: Stefano Cunego <93382903+kukkok3@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:34:47 +0200 Subject: [PATCH 07/12] fix(cat-voices): rename wallet test folder (#856) * fix: remove test folder * fix: test report path * fix: path --- .github/workflows/nightly-ci.yml | 2 +- .../catalyst_cardano/{test => }/wallet-automation/Earthfile | 2 +- .../catalyst_cardano/{test => }/wallet-automation/compose.yml | 0 .../{test => }/wallet-automation/global-setup.ts | 0 .../catalyst_cardano/{test => }/wallet-automation/keys.txt | 0 .../{test => }/wallet-automation/package-lock.json | 0 .../catalyst_cardano/{test => }/wallet-automation/package.json | 0 .../{test => }/wallet-automation/playwright.config.ts | 0 .../catalyst_cardano/{test => }/wallet-automation/utils.ts | 0 .../{test => }/wallet-automation/wallet-tests.spec.ts | 0 10 files changed, 2 insertions(+), 2 deletions(-) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/Earthfile (97%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/compose.yml (100%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/global-setup.ts (100%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/keys.txt (100%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/package-lock.json (100%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/package.json (100%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/playwright.config.ts (100%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/utils.ts (100%) rename catalyst_voices_packages/catalyst_cardano/catalyst_cardano/{test => }/wallet-automation/wallet-tests.spec.ts (100%) diff --git a/.github/workflows/nightly-ci.yml b/.github/workflows/nightly-ci.yml index 367c0af318..71e4bc44db 100644 --- a/.github/workflows/nightly-ci.yml +++ b/.github/workflows/nightly-ci.yml @@ -68,7 +68,7 @@ jobs: if: always() continue-on-error: true with: - earthfile: ./catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/ + earthfile: ./catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/ flags: --allow-privileged targets: nightly-test target_flags: diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/Earthfile b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile similarity index 97% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/Earthfile rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile index ffa8a64a0e..d066ed128b 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/Earthfile +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile @@ -35,7 +35,7 @@ package-test: builder: DO flutter-ci+SETUP - COPY ../../../../../+repo-catalyst-voices-all/repo . + COPY ../../../../+repo-catalyst-voices-all/repo . DO flutter-ci+BOOTSTRAP build-web: diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/compose.yml b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/compose.yml rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/compose.yml diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/global-setup.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/global-setup.ts similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/global-setup.ts rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/global-setup.ts diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/keys.txt b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/keys.txt similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/keys.txt rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/keys.txt diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/package-lock.json b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/package-lock.json rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/package.json b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package.json similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/package.json rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/package.json diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/playwright.config.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/playwright.config.ts similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/playwright.config.ts rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/playwright.config.ts diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/utils.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils.ts similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/utils.ts rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/utils.ts diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/wallet-tests.spec.ts b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/wallet-tests.spec.ts similarity index 100% rename from catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/wallet-tests.spec.ts rename to catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/wallet-tests.spec.ts From 2ccebd685936c71d3ab667f91f14dc3aa09b65df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Moli=C5=84ski?= <47773413+damian-molinski@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:04:05 +0200 Subject: [PATCH 08/12] feat: /m4 redirects to /m4/discovery (#868) Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .../lib/routes/guards/milestone_guard.dart | 11 +++++++++-- catalyst_voices/lib/routes/routing/routes.dart | 14 +++++++------- .../lib/src/session/session_bloc.dart | 7 ++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/catalyst_voices/lib/routes/guards/milestone_guard.dart b/catalyst_voices/lib/routes/guards/milestone_guard.dart index a27f5e7083..e661e57aa6 100644 --- a/catalyst_voices/lib/routes/guards/milestone_guard.dart +++ b/catalyst_voices/lib/routes/guards/milestone_guard.dart @@ -12,13 +12,20 @@ final class MilestoneGuard implements RouteGuard { @override FutureOr redirect(BuildContext context, GoRouterState state) { + final location = state.uri.toString(); + + // redirects /m4 page to /m4/discovery + if (location == '/${Routes.currentMilestone}') { + return const DiscoveryRoute().location; + } + // allow milestone sub pages - if (state.uri.toString().startsWith('/${Routes.currentMilestone}')) { + if (location.startsWith('/${Routes.currentMilestone}')) { return null; } // if already at destination skip redirect - if (state.uri.toString() == const ComingSoonRoute().location) { + if (location == const ComingSoonRoute().location) { return null; } diff --git a/catalyst_voices/lib/routes/routing/routes.dart b/catalyst_voices/lib/routes/routing/routes.dart index e44d80c1a1..11d75a1638 100644 --- a/catalyst_voices/lib/routes/routing/routes.dart +++ b/catalyst_voices/lib/routes/routing/routes.dart @@ -11,14 +11,14 @@ import 'package:go_router/go_router.dart'; abstract final class Routes { static const currentMilestone = 'm4'; + static final List routes = [ + ...coming_soon.$appRoutes, + ...login.$appRoutes, + ...spaces.$appRoutes, + ...overall_spaces.$appRoutes, + ]; + static String get initialLocation { return const coming_soon.ComingSoonRoute().location; } - - static List get routes => [ - ...coming_soon.$appRoutes, - ...login.$appRoutes, - ...spaces.$appRoutes, - ...overall_spaces.$appRoutes, - ]; } diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart index 77545c8e8a..949518e234 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/session/session_bloc.dart @@ -5,7 +5,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// Manages the user session. final class SessionBloc extends Bloc { - SessionBloc() : super(const VisitorSessionState()) { + SessionBloc() + : super( + const ActiveUserSessionState( + user: User(name: 'Account'), + ), + ) { on(_handleSessionEvent); } From 6167e596b5547e77e7fe8ff1ffe64313ddd3a96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Moli=C5=84ski?= <47773413+damian-molinski@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:19:22 +0200 Subject: [PATCH 09/12] feat(cat-voices): local storage + vault (#864) * feat: WIP - interfaces * refactor: move storage to services package + implement codec * feat: More storage types options * chore: docs * feat: secure storage * feat: export storage classes + fix voices usage * test: StorageStringMixin unit tests * test: first secure storage test * secure storage tests * chore: LockFactor tests * chore: codec tests * chore: SecureStorageVault tests * refactor: renaming of lockFactor variables to lock/unlock * refactor: cleanup vault functions order * refactor: Rename Storage mixin * feat: dummy auth storage which encapsulates keys --------- Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .../lib/dependency/dependencies.dart | 8 +- .../src/credentials_storage_repository.dart | 28 +-- .../lib/src/catalyst_voices_services.dart | 8 +- .../src/secure_storage/secure_storage.dart | 2 - .../secure_storage/secure_storage_keys.dart | 6 - .../secure_storage_service.dart | 22 --- .../lib/src/storage/dummy_auth_storage.dart | 47 +++++ .../lib/src/storage/secure_storage.dart | 50 ++++++ .../lib/src/storage/storage.dart | 45 +++++ .../lib/src/storage/storage_string_mixin.dart | 76 ++++++++ .../lib/src/storage/vault/lock_factor.dart | 87 ++++++++++ .../src/storage/vault/lock_factor_codec.dart | 41 +++++ .../storage/vault/secure_storage_vault.dart | 140 +++++++++++++++ .../lib/src/storage/vault/vault.dart | 25 +++ .../catalyst_voices_services/pubspec.yaml | 2 +- .../test/src/storage/secure_storage_test.dart | 80 +++++++++ .../storage/storage_string_mixin_test.dart | 162 ++++++++++++++++++ .../storage/vault/lock_factor_codec_test.dart | 20 +++ .../src/storage/vault/lock_factor_test.dart | 132 ++++++++++++++ .../vault/secure_storage_vault_test.dart | 125 ++++++++++++++ melos.yaml | 1 + 21 files changed, 1052 insertions(+), 55 deletions(-) delete mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage.dart delete mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_keys.dart delete mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_service.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/secure_storage.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage_string_mixin.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/test/src/storage/secure_storage_test.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/test/src/storage/storage_string_mixin_test.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart create mode 100644 catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart diff --git a/catalyst_voices/lib/dependency/dependencies.dart b/catalyst_voices/lib/dependency/dependencies.dart index 589ca073dd..3c9a6b4ba0 100644 --- a/catalyst_voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/lib/dependency/dependencies.dart @@ -33,7 +33,7 @@ final class Dependencies extends DependencyProvider { void _registerRepositories() { this ..registerSingleton( - CredentialsStorageRepository(secureStorageService: get()), + CredentialsStorageRepository(storage: get()), ) ..registerSingleton( AuthenticationRepository(credentialsStorageRepository: get()), @@ -41,8 +41,8 @@ final class Dependencies extends DependencyProvider { } void _registerServices() { - registerSingleton( - SecureStorageService(), - ); + registerSingleton(const SecureStorage()); + registerSingleton(const SecureStorageVault()); + registerSingleton(const SecureDummyAuthStorage()); } } diff --git a/catalyst_voices/packages/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart b/catalyst_voices/packages/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart index 6367bd42a5..25beccc74b 100644 --- a/catalyst_voices/packages/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart +++ b/catalyst_voices/packages/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart @@ -9,21 +9,18 @@ import 'package:result_type/result_type.dart'; /// It will be replaced by a proper implementation as soon as authentication /// flow will be defined. final class CredentialsStorageRepository { - final SecureStorageService secureStorageService; + final DummyAuthStorage _storage; - const CredentialsStorageRepository({required this.secureStorageService}); + const CredentialsStorageRepository({ + required DummyAuthStorage storage, + }) : _storage = storage; - void get clearSessionData => secureStorageService.deleteAll; + Future get clearSessionData async => _storage.clear(); Future> getSessionData() async { try { - final email = await secureStorageService.get( - SecureStorageKeysConst.dummyEmail, - ); - - final password = await secureStorageService.get( - SecureStorageKeysConst.dummyPassword, - ); + final email = await _storage.readEmail(); + final password = await _storage.readPassword(); if (email == null || password == null) { return Success(null); @@ -44,15 +41,8 @@ final class CredentialsStorageRepository { SessionData sessionData, ) async { try { - await secureStorageService.set( - SecureStorageKeysConst.dummyEmail, - sessionData.email, - ); - - await secureStorageService.set( - SecureStorageKeysConst.dummyPassword, - sessionData.password, - ); + await _storage.writeEmail(sessionData.email); + await _storage.writePassword(sessionData.password); return Success(null); } on SecureStorageError catch (_) { return Failure(SecureStorageError.canNotSaveData); diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart index b9cfd362c0..c86d5ab5db 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/catalyst_voices_services.dart @@ -1 +1,7 @@ -export 'secure_storage/secure_storage.dart'; +export 'storage/dummy_auth_storage.dart'; +export 'storage/secure_storage.dart'; +export 'storage/storage.dart'; +export 'storage/vault/lock_factor.dart'; +export 'storage/vault/lock_factor_codec.dart' show LockFactorCodec; +export 'storage/vault/secure_storage_vault.dart'; +export 'storage/vault/vault.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage.dart deleted file mode 100644 index 37c0fba470..0000000000 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'secure_storage_keys.dart'; -export 'secure_storage_service.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_keys.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_keys.dart deleted file mode 100644 index 7a5df87dd4..0000000000 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_keys.dart +++ /dev/null @@ -1,6 +0,0 @@ -final class SecureStorageKeysConst { - static const dummyEmail = 'email'; - static const dummyPassword = 'password'; - - const SecureStorageKeysConst._(); -} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_service.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_service.dart deleted file mode 100644 index 6c506911c9..0000000000 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/secure_storage/secure_storage_service.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - -final class SecureStorageService { - final _storage = const FlutterSecureStorage(); - - SecureStorageService(); - - Future get deleteAll async => _storage.deleteAll(); - - Future delete(String key) { - return _storage.delete(key: key); - } - - Future get(String key) async { - final value = await _storage.read(key: key); - return value; - } - - Future set(String key, String value) async { - return _storage.write(key: key, value: value); - } -} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart new file mode 100644 index 0000000000..74a5674591 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:catalyst_voices_services/src/storage/secure_storage.dart'; + +abstract interface class DummyAuthStorage { + FutureOr readEmail(); + + FutureOr writeEmail(String? value); + + FutureOr readPassword(); + + FutureOr writePassword(String? value); + + Future clear(); +} + +final class SecureDummyAuthStorage extends SecureStorage + implements DummyAuthStorage { + static const _emailKey = 'email'; + static const _passwordKey = 'password'; + + const SecureDummyAuthStorage({ + super.secureStorage, + }); + + @override + FutureOr readEmail() => readString(key: _emailKey); + + @override + FutureOr writeEmail(String? value) { + return writeString(value, key: _emailKey); + } + + @override + FutureOr readPassword() => readString(key: _passwordKey); + + @override + FutureOr writePassword(String? value) { + return writeString(value, key: _passwordKey); + } + + @override + Future clear() async { + await writeEmail(null); + await writePassword(null); + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/secure_storage.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/secure_storage.dart new file mode 100644 index 0000000000..ca9d3aebbd --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/secure_storage.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:catalyst_voices_services/src/storage/storage.dart'; +import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +const _keyPrefix = 'SecureStorage'; + +base class SecureStorage with StorageAsStringMixin implements Storage { + final FlutterSecureStorage _secureStorage; + + const SecureStorage({ + FlutterSecureStorage secureStorage = const FlutterSecureStorage(), + }) : _secureStorage = secureStorage; + + @override + Future readString({required String key}) { + final effectiveKey = _buildVaultKey(key); + + return _secureStorage.read(key: effectiveKey); + } + + @override + Future writeString( + String? value, { + required String key, + }) async { + final effectiveKey = _buildVaultKey(key); + + if (value != null) { + await _secureStorage.write(key: effectiveKey, value: value); + } else { + await _secureStorage.delete(key: effectiveKey); + } + } + + @override + FutureOr clear() async { + final all = await _secureStorage.readAll(); + final vaultKeys = List.of(all.keys).where((e) => e.startsWith(_keyPrefix)); + + for (final key in vaultKeys) { + await _secureStorage.delete(key: key); + } + } + + String _buildVaultKey(String key) { + return '$_keyPrefix.$key'; + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage.dart new file mode 100644 index 0000000000..7e602b3705 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage.dart @@ -0,0 +1,45 @@ +//ignore_for_file: avoid_positional_boolean_parameters + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +/// Abstract representation of generic storage. This interface does +/// not determinate where data should be stored or how it should be stored. +/// Encrypted or not. +/// +/// Implementation may use local memory / filesystem or shared preferences or +/// any other. +abstract interface class Storage { + FutureOr readString({required String key}); + + FutureOr writeString( + String? value, { + required String key, + }); + + FutureOr readInt({required String key}); + + FutureOr writeInt( + int? value, { + required String key, + }); + + FutureOr readBool({required String key}); + + FutureOr writeBool( + bool? value, { + required String key, + }); + + FutureOr readBytes({required String key}); + + FutureOr writeBytes( + Uint8List? value, { + required String key, + }); + + FutureOr delete({required String key}); + + FutureOr clear(); +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage_string_mixin.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage_string_mixin.dart new file mode 100644 index 0000000000..ccb19f245d --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/storage_string_mixin.dart @@ -0,0 +1,76 @@ +//ignore_for_file: avoid_positional_boolean_parameters + +import 'dart:async'; +import 'dart:convert'; + +import 'package:catalyst_voices_services/src/storage/storage.dart'; +import 'package:catalyst_voices_services/src/storage/vault/secure_storage_vault.dart'; +import 'package:flutter/foundation.dart'; + +/// Utility mixin which implements all but String read/write of [Storage] +/// interface. Every method is has its mapping to [readString]/[writeString]. +/// +/// See [SecureStorageVault] as example. +mixin StorageAsStringMixin implements Storage { + @override + FutureOr readInt({required String key}) async { + final value = await readString(key: key); + return value != null ? int.parse(value) : null; + } + + @override + FutureOr writeInt( + int? value, { + required String key, + }) { + return writeString(value?.toString(), key: key); + } + + @override + FutureOr readBool({required String key}) async { + final value = await readInt(key: key); + + return switch (value) { + 0 => false, + 1 => true, + _ => null, + }; + } + + @override + FutureOr writeBool( + bool? value, { + required String key, + }) { + final asInt = value != null + ? value + ? 1 + : 0 + : null; + + return writeInt(asInt, key: key); + } + + @override + FutureOr readBytes({required String key}) async { + final base64String = await readString(key: key); + final bytes = base64String != null + ? Uint8List.fromList(base64Decode(base64String)) + : null; + + return bytes; + } + + @override + FutureOr writeBytes( + Uint8List? value, { + required String key, + }) { + final base64String = value != null ? base64Encode(value) : null; + + return writeString(base64String, key: key); + } + + @override + FutureOr delete({required String key}) => writeString(null, key: key); +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart new file mode 100644 index 0000000000..905e44a570 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor.dart @@ -0,0 +1,87 @@ +import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; + +enum _LockFactorType { voidFactor, password } + +// Note. +// In future we may add MultiLockFactor for bio and password unlock factors + +/// Abstract representation of different factors that can lock [Vault] with. +/// +/// Most common is [PasswordLockFactor] which can be use as standalone factor. +/// +/// This class is serializable to/from json. +sealed class LockFactor { + /// Use [LockFactor.toJson] as parameter for this factory. + factory LockFactor.fromJson(Map json) { + final typeName = json['type']; + final type = _LockFactorType.values.asNameMap()[typeName]; + + return switch (type) { + _LockFactorType.voidFactor => const VoidLockFactor(), + _LockFactorType.password => PasswordLockFactor.fromJson(json), + null => throw ArgumentError('Unknown type name($typeName)', 'json'), + }; + } + + /// Returns true when this [LockFactor] can be used to unlock + /// other [LockFactor]. + bool unlocks(LockFactor factor); + + /// Returns json representation on this [LockFactor]. + /// + /// Should be used with [LockFactor.fromJson]. + Map toJson(); +} + +/// Can not be used to unlock anything. Useful as default value for [LockFactor] +/// variables. +/// +/// [unlocks] always returns false. +final class VoidLockFactor implements LockFactor { + const VoidLockFactor(); + + @override + bool unlocks(LockFactor factor) => false; + + @override + Map toJson() { + return { + 'type': _LockFactorType.voidFactor.name, + }; + } + + @override + String toString() => 'VoidLockFactor'; +} + +/// Password matching [LockFactor]. +/// +/// Only unlocks other [PasswordLockFactor] with matching +/// [PasswordLockFactor._data]. +final class PasswordLockFactor implements LockFactor { + final String _data; + + const PasswordLockFactor(this._data); + + factory PasswordLockFactor.fromJson(Map json) { + return PasswordLockFactor( + json['data'] as String, + ); + } + + @override + bool unlocks(LockFactor factor) { + return factor is PasswordLockFactor && _data == factor._data; + } + + @override + Map toJson() { + return { + 'type': _LockFactorType.password.name, + 'data': _data, + }; + } + + @override + String toString() => 'PasswordLockFactor(data=****)'; +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart new file mode 100644 index 0000000000..c6754ed75e --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/lock_factor_codec.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; + +abstract class LockFactorCodec extends Codec { + const LockFactorCodec(); +} + +/// Uses [LockFactor.toJson] and [LockFactor.fromJson] to serialize to +/// [String] using [json]. +class DefaultLockFactorCodec extends LockFactorCodec { + const DefaultLockFactorCodec(); + + @override + Converter get decoder => const _LockFactorDecoder(); + + @override + Converter get encoder => const _LockFactorEncoder(); +} + +class _LockFactorDecoder extends Converter { + const _LockFactorDecoder(); + + @override + LockFactor convert(String input) { + final json = jsonDecode(input) as Map; + + return LockFactor.fromJson(json); + } +} + +class _LockFactorEncoder extends Converter { + const _LockFactorEncoder(); + + @override + String convert(LockFactor input) { + final json = input.toJson(); + + return jsonEncode(json); + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart new file mode 100644 index 0000000000..024b3ebac0 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart @@ -0,0 +1,140 @@ +import 'dart:async'; + +import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; +import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; +import 'package:catalyst_voices_services/src/storage/vault/lock_factor_codec.dart'; +import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +const _keyPrefix = 'SecureStorageVault'; +const _lockKey = 'LockFactorKey'; +const _unlockKey = 'UnlockFactorKey'; + +// TODO(damian-molinski): Maybe we'll need to encrypt data with LockFactor +/// Implementation of [Vault] that uses [FlutterSecureStorage] as +/// facade for read/write operations. +base class SecureStorageVault with StorageAsStringMixin implements Vault { + final FlutterSecureStorage _secureStorage; + final LockFactorCodec _lockCodec; + + const SecureStorageVault({ + FlutterSecureStorage secureStorage = const FlutterSecureStorage(), + LockFactorCodec lockCodec = const DefaultLockFactorCodec(), + }) : _secureStorage = secureStorage, + _lockCodec = lockCodec; + + /// If storage does not have [LockFactor] this getter will + /// return [VoidLockFactor] as fallback. + Future get _lock => _readLock(_lockKey); + + /// If storage does not have [LockFactor] this getter will + /// return [VoidLockFactor] as fallback. + Future get _unlock => _readLock(_unlockKey); + + @override + Future get isUnlocked async { + final lock = await _lock; + final unlock = await _unlock; + + return unlock.unlocks(lock); + } + + @override + Future lock() => _writeLock(null, key: _unlockKey); + + @override + Future unlock(LockFactor unlock) async { + await _writeLock(unlock, key: _unlockKey); + + return isUnlocked; + } + + @override + Future setLock(LockFactor lock) { + return _writeLock(lock, key: _lockKey); + } + + @override + Future readString({required String key}) => _guardedRead(key: key); + + @override + Future writeString( + String? value, { + required String key, + }) { + return _guardedWrite(value, key: key); + } + + @override + Future clear() async { + final all = await _secureStorage.readAll(); + final vaultKeys = List.of(all.keys).where((e) => e.startsWith(_keyPrefix)); + + for (final key in vaultKeys) { + await _secureStorage.delete(key: key); + } + } + + Future _writeLock( + LockFactor? lock, { + required String key, + }) { + final encodedLock = lock != null ? _lockCodec.encode(lock) : null; + + return _guardedWrite( + encodedLock, + key: key, + requireUnlocked: false, + ); + } + + Future _readLock(String key) async { + final value = await _guardedRead(key: key, requireUnlocked: false); + + return value != null ? _lockCodec.decode(value) : const VoidLockFactor(); + } + + /// Allows operation only when [isUnlocked] it true, otherwise non op. + /// + /// * When [value] is non null writes it to [key]. + /// * When [value] is null then [key] value is deleted. + Future _guardedWrite( + String? value, { + required String key, + bool requireUnlocked = true, + }) async { + final hasAccess = !requireUnlocked || await isUnlocked; + if (!hasAccess) { + return; + } + + final effectiveKey = _buildVaultKey(key); + + if (value != null) { + await _secureStorage.write(key: effectiveKey, value: value); + } else { + await _secureStorage.delete(key: effectiveKey); + } + } + + /// Allows operation only when [isUnlocked] it true, otherwise returns null. + /// + /// Returns value assigned to [key]. May return null if non found for [key]. + Future _guardedRead({ + required String key, + bool requireUnlocked = true, + }) async { + final hasAccess = !requireUnlocked || await isUnlocked; + if (!hasAccess) { + return null; + } + + final effectiveKey = _buildVaultKey(key); + + return _secureStorage.read(key: effectiveKey); + } + + String _buildVaultKey(String key) { + return '$_keyPrefix.$key'; + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart new file mode 100644 index 0000000000..35717e704b --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/storage/vault/vault.dart @@ -0,0 +1,25 @@ +import 'package:catalyst_voices_services/src/storage/storage.dart'; +import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; + +/// Secure version of [Storage] where any read/write methods can take +/// effect only when [isUnlocked] returns true. +/// +/// In order to unlock [Vault] sufficient [LockFactor] have to be +/// set via [unlock] that can unlock [LockFactor] from [setLock]. +/// +/// See [LockFactor.unlocks] for more details. +abstract interface class Vault implements Storage { + /// Returns true when have sufficient [LockFactor] from [unlock]. + Future get isUnlocked; + + /// Deletes unlockFactor if have any. + Future lock(); + + /// Changes [isUnlocked] when [unlock] can unlock [LockFactor] + /// from [setLock]. + Future unlock(LockFactor unlock); + + /// Sets [LockFactor] that which prevents read/write on this [Vault] + /// unless unlocked with matching [LockFactor] via [unlock]. + Future setLock(LockFactor lock); +} diff --git a/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml index 427e6268e9..45a2d329fe 100644 --- a/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_services/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: chopper: ^7.2.0 flutter: sdk: flutter - flutter_secure_storage: ^9.0.0 + flutter_secure_storage: ^9.2.2 json_annotation: ^4.8.1 rxdart: ^0.27.7 diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/secure_storage_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/secure_storage_test.dart new file mode 100644 index 0000000000..72c8921b2d --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/secure_storage_test.dart @@ -0,0 +1,80 @@ +import 'package:catalyst_voices_services/src/storage/secure_storage.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:test/test.dart'; + +void main() { + late final FlutterSecureStorage flutterSecureStorage; + late final SecureStorage secureStorage; + + setUpAll(() { + FlutterSecureStorage.setMockInitialValues({}); + + flutterSecureStorage = const FlutterSecureStorage(); + secureStorage = SecureStorage(secureStorage: flutterSecureStorage); + }); + + tearDown(() async { + await secureStorage.clear(); + }); + + test('read returns null when no value found for key', () async { + // Given + const key = 'key'; + + // When + final value = await secureStorage.readString(key: key); + + // Then + expect(value, isNull); + }); + + test('read returns stored value when has one', () async { + // Given + const key = 'key'; + const expectedValue = 'qqqq'; + + // When + await secureStorage.writeString(expectedValue, key: key); + final value = await secureStorage.readString(key: key); + + // Then + expect(value, expectedValue); + }); + + test('writing null deletes value', () async { + // Given + const key = 'key'; + const expectedValue = 'qqqq'; + + // When + await secureStorage.writeString(expectedValue, key: key); + await secureStorage.writeString(null, key: key); + final value = await secureStorage.readString(key: key); + + // Then + expect(value, isNull); + }); + + test('clear removes all values for this storage', () async { + // Given + const keyValues = { + 'one': 'qqq', + 'two': 'qqq', + }; + + // When + for (final entity in keyValues.entries) { + await secureStorage.writeString(entity.value, key: entity.key); + } + + await secureStorage.clear(); + + final futures = + keyValues.keys.map((e) => secureStorage.readString(key: e)).toList(); + + final values = await Future.wait(futures); + + // Then + expect(values, everyElement(isNull)); + }); +} diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/storage_string_mixin_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/storage_string_mixin_test.dart new file mode 100644 index 0000000000..8792e0243f --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/storage_string_mixin_test.dart @@ -0,0 +1,162 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:catalyst_voices_services/src/storage/storage.dart'; +import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; +import 'package:test/test.dart'; + +void main() { + const key = 'key'; + + final storage = _TestStorage(); + + setUp(storage.clear); + + group('int', () { + test('read returns null when no value found', () async { + // Given + + // When + final value = await storage.readInt(key: key); + + // Then + expect(value, isNull); + }); + + test('read returns non-null when value found', () async { + // Given + const expected = 1; + + // When + storage._data[key] = '$expected'; + final value = await storage.readInt(key: key); + + // Then + expect(value, expected); + }); + + test('internally keeps correct String', () async { + // Given + const expected = 1; + storage._data[key] = '$expected'; + + // When + await storage.writeInt(expected, key: key); + final value = storage.readString(key: key); + + // Then + expect(value, '$expected'); + }); + }); + + group('bool', () { + test('read returns null when no value found', () async { + // Given + + // When + final value = await storage.readBool(key: key); + + // Then + expect(value, isNull); + }); + + test('read stores false as 0', () async { + // Given + const expected = false; + const expectedString = '0'; + + // When + await storage.writeBool(expected, key: key); + final value = await storage.readString(key: key); + + // Then + expect(value, expectedString); + }); + + test('read stores true as 1', () async { + // Given + const expected = true; + const expectedString = '1'; + + // When + await storage.writeBool(expected, key: key); + final value = await storage.readString(key: key); + + // Then + expect(value, expectedString); + }); + + test('write and read values matches', () async { + // Given + const expected = true; + + // When + await storage.writeBool(expected, key: key); + final value = await storage.readBool(key: key); + + // Then + expect(value, expected); + }); + }); + + group('bytes', () { + test('read returns null when no value found', () async { + // Given + + // When + final value = await storage.readBytes(key: key); + + // Then + expect(value, isNull); + }); + + test('can write and read value correctly', () async { + // Given + final bytes = Uint8List.fromList([0, 0, 0, 0, 0, 1]); + + // When + await storage.writeBytes(bytes, key: key); + final value = await storage.readBytes(key: key); + + // Then + expect(value, bytes); + }); + }); + + test('delete writes null string', () async { + // Given + const randomValue = 'D'; + + // When + await storage.writeString(randomValue, key: key); + await storage.delete(key: key); + final value = await storage.readString(key: key); + + // Then + expect(value, isNull); + }); +} + +class _TestStorage with StorageAsStringMixin implements Storage { + final _data = {}; + + @override + FutureOr clear() { + _data.clear(); + } + + @override + FutureOr readString({required String key}) => _data[key]; + + @override + FutureOr writeString( + String? value, { + required String key, + }) { + if (value != null) { + _data[key] = value; + } else { + _data.remove(key); + } + } +} diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart new file mode 100644 index 0000000000..d4857ba8eb --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_codec_test.dart @@ -0,0 +1,20 @@ +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_services/src/storage/vault/lock_factor_codec.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + test('encoding and decoding results in same lock factor', () { + // Given + const lock = PasswordLockFactor('pass1234'); + const LockFactorCodec codec = DefaultLockFactorCodec(); + + // When + final encoded = codec.encoder.convert(lock); + final decoded = codec.decoder.convert(encoded); + + // Then + expect(decoded, isA()); + expect(decoded.unlocks(lock), isTrue); + }); +} diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart new file mode 100644 index 0000000000..3faf9efa23 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/lock_factor_test.dart @@ -0,0 +1,132 @@ +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:test/test.dart'; + +void main() { + group('LockFactor', () { + test('void lock serialization does work', () { + // Given + const lock = VoidLockFactor(); + + // When + final json = lock.toJson(); + final deserializedFactor = LockFactor.fromJson(json); + + // Then + expect(deserializedFactor, isA()); + }); + + test('description', () { + // Given + const lock = PasswordLockFactor('pass1234'); + + // When + final json = lock.toJson(); + final deserializedFactor = LockFactor.fromJson(json); + + // Then + expect(deserializedFactor, isA()); + expect(deserializedFactor.unlocks(lock), isTrue); + }); + }); + + group('VoidLockFactor', () { + test('does not unlocks any other lock', () { + // Given + const lock = VoidLockFactor(); + const locks = [ + VoidLockFactor(), + PasswordLockFactor('pass1234'), + ]; + + // When + final unlocks = locks.map((e) => lock.unlocks(e)).toList(); + + // Then + expect(unlocks, everyElement(false)); + }); + + test('toJson result has type field', () { + // Given + const lock = VoidLockFactor(); + + // When + final json = lock.toJson(); + + // Then + expect(json.containsKey('type'), isTrue); + }); + + test('toString equals class name', () { + // Given + const lock = VoidLockFactor(); + + // When + final asString = lock.toString(); + + // Then + expect(asString, lock.runtimeType.toString()); + }); + }); + + group('PasswordLockFactor', () { + test('unlocks other PasswordLockFactor with matching data', () { + // Given + const lock = PasswordLockFactor('admin1234'); + const otherLock = PasswordLockFactor('admin1234'); + + // When + final unlocks = lock.unlocks(otherLock); + + // Then + expect(unlocks, isTrue); + }); + + test('does not unlocks other PasswordLockFactor with different data', () { + // Given + const lock = PasswordLockFactor('admin1234'); + const otherLock = PasswordLockFactor('pass1234'); + + // When + final unlocks = lock.unlocks(otherLock); + + // Then + expect(unlocks, isFalse); + }); + + test('does not unlocks other non PasswordLockFactor', () { + // Given + const lock = PasswordLockFactor('admin1234'); + const otherLock = VoidLockFactor(); + + // When + final unlocks = lock.unlocks(otherLock); + + // Then + expect(unlocks, isFalse); + }); + + test('toJson result has type and data field', () { + // Given + const lock = PasswordLockFactor('admin1234'); + + // When + final json = lock.toJson(); + + // Then + expect(json.containsKey('type'), isTrue); + expect(json.containsKey('data'), isTrue); + }); + + test('toString does not contain password', () { + // Given + const password = 'admin1234'; + const lock = PasswordLockFactor(password); + + // When + final asString = lock.toString(); + + // Then + expect(asString, isNot(contains(password))); + }); + }); +} diff --git a/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart new file mode 100644 index 0000000000..cfba41eb73 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart @@ -0,0 +1,125 @@ +import 'package:catalyst_voices_services/src/storage/vault/lock_factor.dart'; +import 'package:catalyst_voices_services/src/storage/vault/secure_storage_vault.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:test/test.dart'; + +void main() { + late final FlutterSecureStorage flutterSecureStorage; + late final SecureStorageVault vault; + + setUpAll(() { + FlutterSecureStorage.setMockInitialValues({}); + + flutterSecureStorage = const FlutterSecureStorage(); + vault = SecureStorageVault(secureStorage: flutterSecureStorage); + }); + + tearDown(() async { + await flutterSecureStorage.deleteAll(); + }); + + test('lock and unlock factor fallbacks to lock state', () async { + // Given + + // When + final isUnlocked = await vault.isUnlocked; + + // Then + expect(isUnlocked, isFalse); + }); + + test('read returns null when not unlocked', () async { + // Given + const key = 'SecureStorageVault.key'; + const value = 'username'; + + // When + await flutterSecureStorage.write(key: key, value: value); + final readValue = await vault.readString(key: key); + + // Then + expect(readValue, isNull); + }); + + test('write wont happen when is locked', () async { + // Given + const key = 'key'; + const fKey = 'SecureStorageVault.$key'; + const value = 'username'; + + // When + await vault.writeString(value, key: key); + final readValue = await flutterSecureStorage.read(key: fKey); + + // Then + expect(readValue, isNull); + }); + + test('unlock update lock and returns null when locked', () async { + // Given + const lock = PasswordLockFactor('pass1234'); + const key = 'key'; + const value = 'username'; + + // When + await vault.setLock(lock); + final isUnlocked = await vault.unlock(lock); + await vault.writeString(value, key: key); + final readValue = await vault.readString(key: key); + + // Then + expect(isUnlocked, isTrue); + expect(readValue, value); + }); + + test('lock makes vault locked', () async { + // Given + const lock = PasswordLockFactor('pass1234'); + + // When + await vault.setLock(lock); + await vault.unlock(lock); + await vault.lock(); + + final isUnlocked = await vault.isUnlocked; + + // Then + expect(isUnlocked, isFalse); + }); + + test('clear removes all vault keys', () async { + // Given + const lock = PasswordLockFactor('pass1234'); + const vaultKeyValues = { + 'one': 'qqq', + 'two': 'qqq', + }; + const nonVaultKeyValues = { + 'three': 'qqq', + }; + + // When + await vault.setLock(lock); + await vault.unlock(lock); + + for (final entity in vaultKeyValues.entries) { + await vault.writeString(entity.value, key: entity.key); + } + + for (final entity in nonVaultKeyValues.entries) { + await flutterSecureStorage.write(key: entity.key, value: entity.value); + } + + await vault.clear(); + + final futures = + vaultKeyValues.keys.map((e) => vault.readString(key: e)).toList(); + + final values = await Future.wait(futures); + final fValues = await flutterSecureStorage.readAll(); + + // Then + expect(values, everyElement(isNull)); + expect(fValues, nonVaultKeyValues); + }); +} diff --git a/melos.yaml b/melos.yaml index 345b88fae7..4a40c3aa22 100644 --- a/melos.yaml +++ b/melos.yaml @@ -28,6 +28,7 @@ command: flutter_localized_locales: ^2.0.5 flutter_quill: ^10.5.13 flutter_quill_extensions: ^10.5.13 + flutter_secure_storage: ^9.2.2 formz: ^0.7.0 intl: ^0.19.0 logging: ^1.2.0 From ac764d6b011b398a949c998987f570695e59db4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Moli=C5=84ski?= <47773413+damian-molinski@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:31:10 +0200 Subject: [PATCH 10/12] feat: discovery cta modal (#869) * feat: discovery alert dialog * feat: dialog barrier color --- .../lib/pages/discovery/discovery_page.dart | 13 ++++++++++++- .../lib/src/themes/catalyst.dart | 5 ++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/catalyst_voices/lib/pages/discovery/discovery_page.dart b/catalyst_voices/lib/pages/discovery/discovery_page.dart index 44dc96f11b..7049932a0d 100644 --- a/catalyst_voices/lib/pages/discovery/discovery_page.dart +++ b/catalyst_voices/lib/pages/discovery/discovery_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:catalyst_voices/pages/discovery/current_status_text.dart'; import 'package:catalyst_voices/pages/discovery/toggle_state_text.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; @@ -84,7 +86,16 @@ class _Segment extends StatelessWidget { const Spacer(), VoicesFilledButton( child: const Text('CTA to Model'), - onTap: () {}, + onTap: () { + unawaited( + VoicesDialog.show( + context: context, + builder: (context) { + return const VoicesDesktopInfoDialog(title: Text('')); + }, + ), + ); + }, ), ], ), diff --git a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart index 52149626e6..1e76d0c85a 100644 --- a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart +++ b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart @@ -310,9 +310,8 @@ ThemeData _buildThemeData( backgroundColor: voicesColorScheme.onSurfaceNeutralOpaqueLv0, ), dialogTheme: DialogTheme( - // TODO(damian-molinski): themed value needed. - // We don't have it defined yet. - barrierColor: const Color(0x612A3D61), + // N10-38 + barrierColor: const Color(0x212A3D61), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), clipBehavior: Clip.hardEdge, backgroundColor: voicesColorScheme.elevationsOnSurfaceNeutralLv1White, From d728f10f0d3c66e04d9fdb722944a5c6869c0cec Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Mon, 23 Sep 2024 11:52:01 -0700 Subject: [PATCH 11/12] ci(general): integrates catalyst-forge (#847) --- .github/workflows/ci.yml | 11 +--- blueprint.cue | 51 +++++++++++++++++++ catalyst-gateway/blueprint.cue | 2 + catalyst-gateway/event-db/blueprint.cue | 2 + .../tests/api_tests/blueprint.cue | 2 + catalyst-gateway/tests/blueprint.cue | 2 + .../tests/schemathesis_tests/blueprint.cue | 2 + catalyst_voices/Earthfile | 6 ++- catalyst_voices/blueprint.cue | 2 + .../test/wallet-automation/blueprint.cue | 2 + docs/blueprint.cue | 2 + utilities/wallet-tester/blueprint.cue | 2 + 12 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 blueprint.cue create mode 100644 catalyst-gateway/blueprint.cue create mode 100644 catalyst-gateway/event-db/blueprint.cue create mode 100644 catalyst-gateway/tests/api_tests/blueprint.cue create mode 100644 catalyst-gateway/tests/blueprint.cue create mode 100644 catalyst-gateway/tests/schemathesis_tests/blueprint.cue create mode 100644 catalyst_voices/blueprint.cue create mode 100644 catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/blueprint.cue create mode 100644 docs/blueprint.cue create mode 100644 utilities/wallet-tester/blueprint.cue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a83c0527f..0d887d7800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,13 +10,6 @@ permissions: jobs: ci: - uses: input-output-hk/catalyst-ci/.github/workflows/ci.yml@master + uses: input-output-hk/catalyst-forge/.github/workflows/ci.yml@master with: - aws_ecr_registry: 332405224602.dkr.ecr.eu-central-1.amazonaws.com - aws_role_arn: arn:aws:iam::332405224602:role/ci - aws_region: eu-central-1 - secrets: - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} - dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} - earthly_runner_address: ${{ secrets.EARTHLY_SATELLITE_ADDRESS }} - earthly_runner_secret: ${{ secrets.EARTHLY_RUNNER_SECRET }} \ No newline at end of file + forge_version: latest \ No newline at end of file diff --git a/blueprint.cue b/blueprint.cue new file mode 100644 index 0000000000..193d3916f6 --- /dev/null +++ b/blueprint.cue @@ -0,0 +1,51 @@ +version: "1.0" +global: { + ci: { + local: [ + "^check(-.*)?$", + "^build(-.*)?$", + "^package(-.*)?$", + "^test(-.*)?$", + "^release(-.*)?$", + "^publish(-.*)?$", + ] + registries: [ + ci.providers.aws.registry, + ] + providers: { + aws: { + region: "eu-central-1" + registry: "332405224602.dkr.ecr.eu-central-1.amazonaws.com" + role: "arn:aws:iam::332405224602:role/ci" + } + + docker: credentials: { + provider: "aws" + path: "global/ci/docker" + } + + earthly: { + credentials: { + provider: "aws" + path: "global/ci/earthly" + } + org: "Catalyst" + satellite: "ci" + version: "0.8.15" + } + + github: registry: "ghcr.io" + } + secrets: [ + { + name: "GITHUB_TOKEN" + optional: true + provider: "env" + path: "GITHUB_TOKEN" + }, + ] + tagging: { + strategy: "commit" + } + } +} diff --git a/catalyst-gateway/blueprint.cue b/catalyst-gateway/blueprint.cue new file mode 100644 index 0000000000..3278906156 --- /dev/null +++ b/catalyst-gateway/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "gateway" diff --git a/catalyst-gateway/event-db/blueprint.cue b/catalyst-gateway/event-db/blueprint.cue new file mode 100644 index 0000000000..1dad7a98b0 --- /dev/null +++ b/catalyst-gateway/event-db/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "gateway-event-db" diff --git a/catalyst-gateway/tests/api_tests/blueprint.cue b/catalyst-gateway/tests/api_tests/blueprint.cue new file mode 100644 index 0000000000..d15bf941d5 --- /dev/null +++ b/catalyst-gateway/tests/api_tests/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "gateway-api-tests" diff --git a/catalyst-gateway/tests/blueprint.cue b/catalyst-gateway/tests/blueprint.cue new file mode 100644 index 0000000000..17b8489385 --- /dev/null +++ b/catalyst-gateway/tests/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "catalyst-gateway-tests" diff --git a/catalyst-gateway/tests/schemathesis_tests/blueprint.cue b/catalyst-gateway/tests/schemathesis_tests/blueprint.cue new file mode 100644 index 0000000000..4a26b5160d --- /dev/null +++ b/catalyst-gateway/tests/schemathesis_tests/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "gateway-schema-tests" diff --git a/catalyst_voices/Earthfile b/catalyst_voices/Earthfile index 989dbce65b..f22c850de5 100644 --- a/catalyst_voices/Earthfile +++ b/catalyst_voices/Earthfile @@ -83,6 +83,8 @@ package: publish: FROM +package - ARG tag='latest' - SAVE IMAGE voices-frontend:$tag \ No newline at end of file + ARG container="voices" + ARG tag="latest" + + SAVE IMAGE ${container}:${tag} \ No newline at end of file diff --git a/catalyst_voices/blueprint.cue b/catalyst_voices/blueprint.cue new file mode 100644 index 0000000000..c76d4145ee --- /dev/null +++ b/catalyst_voices/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "voices" diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/blueprint.cue b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/blueprint.cue new file mode 100644 index 0000000000..cb4463fa63 --- /dev/null +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/test/wallet-automation/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "voices-wallet-automation-test" diff --git a/docs/blueprint.cue b/docs/blueprint.cue new file mode 100644 index 0000000000..d5db755573 --- /dev/null +++ b/docs/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "voices-docs" diff --git a/utilities/wallet-tester/blueprint.cue b/utilities/wallet-tester/blueprint.cue new file mode 100644 index 0000000000..263a5d2095 --- /dev/null +++ b/utilities/wallet-tester/blueprint.cue @@ -0,0 +1,2 @@ +version: "1.0.0" +project: name: "voices-wallet-tester" From d972a977a0bd2d660b5138d7600849d9b1869845 Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj <38898766+apskhem@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:28:49 +0700 Subject: [PATCH 12/12] ci(cat-voices): Add license checker (#865) * feat: initial checks * fix: add -i flag * feat: new ci version * chore: bump ci version --------- Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- catalyst_voices/Earthfile | 7 +- catalyst_voices/uikit_example/Earthfile | 2 +- .../wallet-automation/Earthfile | 2 +- melos.yaml | 66 +++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/catalyst_voices/Earthfile b/catalyst_voices/Earthfile index f22c850de5..ff6b0d1c25 100644 --- a/catalyst_voices/Earthfile +++ b/catalyst_voices/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 IMPORT ../catalyst-gateway AS catalyst-gateway -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.04 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.06 AS flutter-ci # Copy all the necessary files and running bootstrap builder: @@ -52,6 +52,11 @@ check-package-publishing: FROM +builder DO flutter-ci+PUBLISH_DRY_RUN +# Runs the license checks for Catalyst Voices. +check-license: + FROM +builder + RUN melos run license-check + # Run unit tests test-unit: FROM +builder diff --git a/catalyst_voices/uikit_example/Earthfile b/catalyst_voices/uikit_example/Earthfile index 309d129344..c5e6381e6d 100644 --- a/catalyst_voices/uikit_example/Earthfile +++ b/catalyst_voices/uikit_example/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 IMPORT ../ AS catalyst-voices -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.04 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.06 AS flutter-ci # local-build-web - build web version of UIKit example. # Prefixed by "local" to make sure it's not auto triggered, the target was diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile index d066ed128b..37f8843afa 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile @@ -1,5 +1,5 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.04 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.06 AS flutter-ci deps: FROM mcr.microsoft.com/playwright:v1.45.2-jammy diff --git a/melos.yaml b/melos.yaml index 4a40c3aa22..e8d7e36277 100644 --- a/melos.yaml +++ b/melos.yaml @@ -8,6 +8,67 @@ packages: - catalyst_voices_packages/** - utilities/** +permittedLicenses: +- MIT +- Apache-2.0 +- Unicode-DFS-2016 +- BSD-3-Clause +- BSD-2-Clause +- BlueOak-1.0.0 +- Apache-2.0 WITH LLVM-exception +- CC0-1.0 + +packageLicenseOverride: + fuchsia_remote_debug_protocol: BSD-3-Clause + flutter_driver: BSD-3-Clause + flutter_localizations: BSD-3-Clause + flutter_test: BSD-3-Clause + flutter_web_plugins: BSD-3-Clause + integration_test: BSD-3-Clause + rxdart: Apache-2.0 + vector_math: Apache-2.0 + sky_engine: Apache-2.0 + flutter_gen: MIT + # catalyst packages + catalyst_analysis: Apache-2.0 + catalyst_cose: Apache-2.0 + catalyst_cardano_serialization: Apache-2.0 + catalyst_cardano_platform_interface: Apache-2.0 + catalyst_compression: Apache-2.0 + catalyst_compression_web: Apache-2.0 + catalyst_cardano_web: Apache-2.0 + catalyst_compression_platform_interface: Apache-2.0 + catalyst_voices_assets: Apache-2.0 + catalyst_voices_assets_example: Apache-2.0 + catalyst_cardano: Apache-2.0 + catalyst_cardano_example: Apache-2.0 + catalyst_voices_localization: Apache-2.0 + catalyst_voices_models: Apache-2.0 + example: Apache-2.0 + catalyst_voices_remote_widgets: Apache-2.0 + remote_widgets_example: Apache-2.0 + catalyst_voices_view_models: Apache-2.0 + catalyst_voices_blocs: Apache-2.0 + catalyst_voices_shared: Apache-2.0 + catalyst_voices_brands: Apache-2.0 + catalyst_voices_services: Apache-2.0 + poc_local_storage: Apache-2.0 + catalyst_voices_repositories: Apache-2.0 + catalyst_voices: Apache-2.0 + uikit_example: Apache-2.0 + +packageSourceOverride: + fuchsia_remote_debug_protocol: https://github.com/flutter/flutter/blob/main/LICENSE + flutter_driver: https://github.com/flutter/flutter/blob/main/LICENSE + flutter_localizations: https://github.com/flutter/flutter/blob/main/LICENSE + flutter_test: https://github.com/flutter/flutter/blob/main/LICENSE + flutter_web_plugins: https://github.com/flutter/flutter/blob/main/LICENSE + integration_test: https://github.com/flutter/flutter/blob/main/LICENSE + rxdart: https://github.com/ReactiveX/rxdart/blob/master/LICENSE + vector_math: https://github.com/google/vector_math.dart/blob/master/LICENSE + sky_engine: https://github.com/flutter/engine/blob/main/sky/packages/sky_engine/LICENSE + flutter_gen: https://github.com/FlutterGen/flutter_gen/blob/main/LICENSE + command: version: linkToCommits: true @@ -76,6 +137,11 @@ scripts: melos exec -c 1 --dir-exists="integration_test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' | tr '\n' ' ' | xargs dart format --output none --set-exit-if-changed" description: Run `dart format` checks for all packages. + license-check: + run: | + melos exec -- lic_ck check-licenses -c $MELOS_ROOT_PATH/melos.yaml -i + description: Run `lic_ck` checks for all packages. + test:select: run: | melos exec -c 1 --dir-exists="test" -- flutter test