From 05c5c9c9a76e594b92378fba4aa3fa1fc9f854ba Mon Sep 17 00:00:00 2001 From: Dominik Toton <166132265+dtscalac@users.noreply.github.com> Date: Fri, 20 Sep 2024 19:19:00 +0200 Subject: [PATCH 1/7] feat(cat-voices): alert dialog (#851) * feat(cat-voices): alert dialog * fix(cat-voices) pass isDismissible to alert dialog * refactor(cat-voices): cleanup * chore(cat-voices): tests --- .../lib/widgets/avatars/voices_avatar.dart | 17 +- .../widgets/modals/voices_alert_dialog.dart | 163 ++++++++++++++++++ .../widgets/modals/voices_desktop_dialog.dart | 3 - .../lib/widgets/modals/voices_dialog.dart | 8 +- .../widgets/modals/voices_info_dialog.dart | 10 +- catalyst_voices/lib/widgets/widgets.dart | 1 + .../lib/src/themes/catalyst.dart | 2 +- .../widgets/avatars/voices_avatar_test.dart | 40 +++-- .../lib/examples/voices_avatar_example.dart | 8 + .../lib/examples/voices_modals_example.dart | 53 +++++- 10 files changed, 268 insertions(+), 37 deletions(-) create mode 100644 catalyst_voices/lib/widgets/modals/voices_alert_dialog.dart diff --git a/catalyst_voices/lib/widgets/avatars/voices_avatar.dart b/catalyst_voices/lib/widgets/avatars/voices_avatar.dart index b2570c6ed5..8989efd60a 100644 --- a/catalyst_voices/lib/widgets/avatars/voices_avatar.dart +++ b/catalyst_voices/lib/widgets/avatars/voices_avatar.dart @@ -21,6 +21,9 @@ class VoicesAvatar extends StatelessWidget { /// The size of the avatar, expressed as the radius (half the diameter). final double radius; + /// The border around the widget. + final Border? border; + /// The callback called when the widget is tapped. final VoidCallback? onTap; @@ -32,15 +35,21 @@ class VoicesAvatar extends StatelessWidget { this.backgroundColor, this.padding = const EdgeInsets.all(8), this.radius = 20, + this.border, this.onTap, }); @override Widget build(BuildContext context) { - return CircleAvatar( - radius: radius, - backgroundColor: - backgroundColor ?? Theme.of(context).colorScheme.primaryContainer, + return Container( + width: radius * 2, + height: radius * 2, + decoration: BoxDecoration( + color: + backgroundColor ?? Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(radius), + border: border, + ), child: Material( type: MaterialType.transparency, child: InkWell( diff --git a/catalyst_voices/lib/widgets/modals/voices_alert_dialog.dart b/catalyst_voices/lib/widgets/modals/voices_alert_dialog.dart new file mode 100644 index 0000000000..654260c71f --- /dev/null +++ b/catalyst_voices/lib/widgets/modals/voices_alert_dialog.dart @@ -0,0 +1,163 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; + +/// An alert dialog similar to [AlertDialog] +/// but customized to the project needs. +/// +/// On extra small screens (mobile) it will fill the whole screen width, +/// on larger screens it will take [_width] amount +/// of horizontal space and be centered. +/// +/// The close (x) button will appear if the dialog [isDismissible]. +class VoicesAlertDialog extends StatelessWidget { + static const double _width = 360; + + /// The widget which appears at the top of the dialog next to the (x) button. + /// Usually a [Text] widget. + final Widget? title; + + /// The widget which appears below the [title], + /// usually a [VoicesAvatar] widget. + final Widget? icon; + + /// The widget appears below the [icon], is less prominent than the [title]. + /// Usually a [Text] widget. + final Widget? subtitle; + + /// The widget appears below the [subtitle], usually a [Text] widget, + /// can be multiline. + final Widget? content; + + /// The list of widgets which appear at the bottom of the dialog, + /// usually [VoicesFilledButton] or [VoicesTextButton]. + /// + /// [buttons] are separated with 8px of padding between each other + /// so you don't need to add your own padding. + final List buttons; + + /// Whether to show a (x) close button. + final bool isDismissible; + + const VoicesAlertDialog({ + super.key, + this.title, + this.icon, + this.subtitle, + this.content, + this.buttons = const [], + this.isDismissible = true, + }); + + @override + Widget build(BuildContext context) { + final title = this.title; + final icon = this.icon; + final subtitle = this.subtitle; + final content = this.content; + + return ResponsiveBuilder( + xs: double.infinity, + other: _width, + builder: (context, width) { + return Dialog( + alignment: Alignment.center, + child: SizedBox( + width: width, + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (title != null || isDismissible) + Row( + children: [ + // if widget is dismissible then show an invisible + // close button to reserve space on this side of the + // row so that the title is centered + if (isDismissible) + const Visibility( + visible: false, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: _CloseButton(), + ), + Expanded( + child: DefaultTextStyle( + style: Theme.of(context).textTheme.titleLarge!, + textAlign: TextAlign.center, + child: title ?? const SizedBox.shrink(), + ), + ), + if (isDismissible) const _CloseButton(), + ], + ), + if (icon != null) + Padding( + padding: const EdgeInsets.only( + top: 24, + left: 20, + right: 20, + ), + child: Center(child: icon), + ), + if (subtitle != null) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 20, + right: 20, + ), + child: DefaultTextStyle( + style: Theme.of(context).textTheme.titleSmall!, + textAlign: TextAlign.center, + child: subtitle, + ), + ), + if (content != null) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 20, + right: 20, + ), + child: DefaultTextStyle( + style: Theme.of(context).textTheme.bodyMedium!, + textAlign: TextAlign.center, + child: content, + ), + ), + if (buttons.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + ...buttons.separatedBy(const SizedBox(height: 8)), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class _CloseButton extends StatelessWidget { + const _CloseButton(); + + @override + Widget build(BuildContext context) { + return XButton( + onTap: () => Navigator.of(context).pop(), + ); + } +} diff --git a/catalyst_voices/lib/widgets/modals/voices_desktop_dialog.dart b/catalyst_voices/lib/widgets/modals/voices_desktop_dialog.dart index 3016043a40..8318dbbe20 100644 --- a/catalyst_voices/lib/widgets/modals/voices_desktop_dialog.dart +++ b/catalyst_voices/lib/widgets/modals/voices_desktop_dialog.dart @@ -62,9 +62,6 @@ class VoicesDesktopPanelsDialog extends StatelessWidget { Expanded( child: Container( padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: theme.colors.elevationsOnSurfaceNeutralLv1White, - ), child: right, ), ), diff --git a/catalyst_voices/lib/widgets/modals/voices_dialog.dart b/catalyst_voices/lib/widgets/modals/voices_dialog.dart index da2b7de363..95b0cd4976 100644 --- a/catalyst_voices/lib/widgets/modals/voices_dialog.dart +++ b/catalyst_voices/lib/widgets/modals/voices_dialog.dart @@ -4,13 +4,17 @@ import 'package:flutter/material.dart'; /// meant to be extended. abstract final class VoicesDialog { /// Encapsulates single entry point. - static Future show( - BuildContext context, { + static Future show({ + required BuildContext context, required WidgetBuilder builder, + RouteSettings? routeSettings, + bool barrierDismissible = true, }) { return showDialog( context: context, builder: builder, + routeSettings: routeSettings, + barrierDismissible: barrierDismissible, ); } } diff --git a/catalyst_voices/lib/widgets/modals/voices_info_dialog.dart b/catalyst_voices/lib/widgets/modals/voices_info_dialog.dart index 1bb023866f..d81bd7b909 100644 --- a/catalyst_voices/lib/widgets/modals/voices_info_dialog.dart +++ b/catalyst_voices/lib/widgets/modals/voices_info_dialog.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; /// Call [VoicesDialog.show] with [VoicesDesktopInfoDialog] in order /// to show it. class VoicesDesktopInfoDialog extends StatelessWidget { - final String title; + final Widget title; const VoicesDesktopInfoDialog({ super.key, @@ -26,10 +26,10 @@ class VoicesDesktopInfoDialog extends StatelessWidget { left: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: theme.textTheme.titleLarge - ?.copyWith(color: theme.colors.textOnPrimary), + DefaultTextStyle( + style: theme.textTheme.titleLarge! + .copyWith(color: theme.colors.textOnPrimary), + child: title, ), ], ), diff --git a/catalyst_voices/lib/widgets/widgets.dart b/catalyst_voices/lib/widgets/widgets.dart index 7b0172eefd..0b9e550ea8 100644 --- a/catalyst_voices/lib/widgets/widgets.dart +++ b/catalyst_voices/lib/widgets/widgets.dart @@ -39,6 +39,7 @@ export 'menu/voices_list_tile.dart'; export 'menu/voices_menu.dart'; export 'menu/voices_node_menu.dart'; export 'menu/voices_wallet_tile.dart'; +export 'modals/voices_alert_dialog.dart'; export 'modals/voices_desktop_dialog.dart'; export 'modals/voices_dialog.dart'; export 'modals/voices_info_dialog.dart'; 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 dae4e3f1da..52149626e6 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 @@ -315,7 +315,7 @@ ThemeData _buildThemeData( barrierColor: const Color(0x612A3D61), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), clipBehavior: Clip.hardEdge, - backgroundColor: voicesColorScheme.onSurfaceNeutralOpaqueLv0, + backgroundColor: voicesColorScheme.elevationsOnSurfaceNeutralLv1White, ), listTileTheme: ListTileThemeData( shape: const StadiumBorder(), diff --git a/catalyst_voices/test/widgets/avatars/voices_avatar_test.dart b/catalyst_voices/test/widgets/avatars/voices_avatar_test.dart index 1c98c6f849..757f804687 100644 --- a/catalyst_voices/test/widgets/avatars/voices_avatar_test.dart +++ b/catalyst_voices/test/widgets/avatars/voices_avatar_test.dart @@ -14,13 +14,12 @@ void main() { ), ); - // Verify if CircleAvatar is rendered with the correct default radius. - final circleAvatarFinder = find.byType(CircleAvatar); - expect(circleAvatarFinder, findsOneWidget); + // Verify if Container is rendered with the correct default radius. + final containerFinder = find.byType(Container); + expect(containerFinder, findsOneWidget); - final circleAvatarWidget = - tester.widget(circleAvatarFinder); - expect(circleAvatarWidget.radius, 20); + final containerWidget = tester.widget(containerFinder); + expect(containerWidget.constraints?.maxWidth, 40); // Verify the icon is rendered. expect(find.byIcon(Icons.person), findsOneWidget); @@ -40,11 +39,10 @@ void main() { ), ); - // Verify if CircleAvatar is rendered with the correct custom radius. - final circleAvatarFinder = find.byType(CircleAvatar); - final circleAvatarWidget = - tester.widget(circleAvatarFinder); - expect(circleAvatarWidget.radius, 30); + // Verify if Container is rendered with the correct custom radius. + final containerFinder = find.byType(Container); + final containerWidget = tester.widget(containerFinder); + expect(containerWidget.constraints?.maxWidth, 60); // Verify the Padding is applied correctly. final paddingFinder = find.ancestor( @@ -73,10 +71,12 @@ void main() { ); // Verify the background color is correctly applied. - final circleAvatarFinder = find.byType(CircleAvatar); - final circleAvatarWidget = - tester.widget(circleAvatarFinder); - expect(circleAvatarWidget.backgroundColor, backgroundColor); + final containerFinder = find.byType(Container); + final containerWidget = tester.widget(containerFinder); + expect( + (containerWidget.decoration! as BoxDecoration).color, + backgroundColor, + ); // Verify the foreground color is correctly applied to the icon. final iconThemeFinder = find.ancestor( @@ -132,10 +132,12 @@ void main() { ); // Verify the background color is from the theme's primaryContainer. - final circleAvatarFinder = find.byType(CircleAvatar); - final circleAvatarWidget = - tester.widget(circleAvatarFinder); - expect(circleAvatarWidget.backgroundColor, Colors.blueGrey); + final containerFinder = find.byType(Container); + final containerWidget = tester.widget(containerFinder); + expect( + (containerWidget.decoration! as BoxDecoration).color, + Colors.blueGrey, + ); // Verify the foreground color is from the theme's primary. final iconThemeFinder = find.byType(IconTheme); diff --git a/catalyst_voices/uikit_example/lib/examples/voices_avatar_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_avatar_example.dart index 53bbee07ec..5c1c42ae9a 100644 --- a/catalyst_voices/uikit_example/lib/examples/voices_avatar_example.dart +++ b/catalyst_voices/uikit_example/lib/examples/voices_avatar_example.dart @@ -22,6 +22,14 @@ class VoicesAvatarExample extends StatelessWidget { VoicesAvatar( icon: VoicesAssets.icons.check.buildIcon(), ), + VoicesAvatar( + backgroundColor: Colors.transparent, + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + icon: VoicesAssets.icons.check.buildIcon(), + ), VoicesAvatar( icon: const Text('A'), onTap: () {}, diff --git a/catalyst_voices/uikit_example/lib/examples/voices_modals_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_modals_example.dart index 304b0626ad..79aca47497 100644 --- a/catalyst_voices/uikit_example/lib/examples/voices_modals_example.dart +++ b/catalyst_voices/uikit_example/lib/examples/voices_modals_example.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + 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'; class VoicesModalsExample extends StatelessWidget { @@ -12,21 +16,64 @@ class VoicesModalsExample extends StatelessWidget { appBar: AppBar(title: const Text('Modals')), body: Padding( padding: const EdgeInsets.all(16), - child: Column( + child: Wrap( + spacing: 16, + runSpacing: 16, children: [ VoicesFilledButton( child: const Text('Desktop info dialog'), onTap: () async { await VoicesDialog.show( - context, + context: context, builder: (context) { return const VoicesDesktopInfoDialog( - title: 'Desktop modal', + title: Text('Desktop modal'), ); }, ); }, ), + VoicesFilledButton( + child: const Text('Alert Dialog'), + onTap: () => unawaited( + VoicesDialog.show( + context: context, + builder: (context) { + return VoicesAlertDialog( + title: const Text('WARNING'), + icon: VoicesAvatar( + radius: 40, + backgroundColor: Colors.transparent, + icon: VoicesAssets.icons.exclamation.buildIcon( + size: 36, + color: Theme.of(context).colors.iconsError, + ), + border: Border.all( + color: Theme.of(context).colors.iconsError!, + width: 3, + ), + ), + subtitle: const Text('ACCOUNT CREATION INCOMPLETE!'), + content: const Text( + 'If attempt to leave without creating your keychain' + ' - account creation will be incomplete.\n\nYou are' + ' not able to login without completing your keychain.', + ), + buttons: [ + VoicesFilledButton( + child: const Text('Continue keychain creation'), + onTap: () => Navigator.of(context).pop(), + ), + VoicesTextButton( + child: const Text('Cancel anyway'), + onTap: () => Navigator.of(context).pop(), + ), + ], + ); + }, + ), + ), + ), ], ), ), From b96002e74d10a88054dd5e177f90c42e2fd19a6c Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Sat, 21 Sep 2024 08:01:10 +0700 Subject: [PATCH 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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), - ); - } -}