diff --git a/didkit_setup.sh b/didkit_setup.sh new file mode 100755 index 00000000..5ee12692 --- /dev/null +++ b/didkit_setup.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +if [[ "$*" != *-android* ]] && [[ "$*" != *-ios* ]]; then + echo -e "\033[0;31mAt least one of the following arguments are required to build didkit:\033[0m + \033[0;36m-android\033[0m: builds didkit's Android binaries + \033[0;36m-ios\033[0m: builds didkit's iOS binaries +" + exit +fi + +if [[ "$OSTYPE" == "darwin"* ]]; then + echo "Checking for brew installation." + if ! command -v brew &>/dev/null; then + echo -e "\033[0;Could not find brew, please install brew or add it to path.\033[0m" + exit + fi +fi + +echo "Checking for rustup installation and setting rust to nightly." +if ! command -v rustup &>/dev/null; then + if ! command -v curl &>/dev/null; then + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install curl + else + sudo apt install curl -yet + fi + fi + + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + rustup default nightly +fi + +if [[ "$*" == *-android* ]]; then + echo "Checking for java installation." + + if ! command -v javac &>/dev/null; then + if [[ "$OSTYPE" == "darwin"* ]]; then + brew install openjdk + else + sudo apt install default-jdk + fi + fi +fi + +echo "Cloning DIDKit repo if not yet on previous directory" +[ ! -d "../didkit" ] && git clone https://github.com/spruceid/didkit.git ../didkit + +echo "Cloning SSI repo if not yet on previous directory" +[ ! -d "../ssi" ] && git clone https://github.com/spruceid/ssi.git --recurse-submodules ../ssi + +if [[ "$*" == *-android* ]]; then + echo "Checking for android sdk." + [ ! -d "$ANDROID_SDK_ROOT" ] && echo -e "\033[0;31mFailed to find Android SDK\033[0m" && exit + [ ! -d "$ANDROID_SDK_ROOT/build-tools" ] && [ ! -d "$ANDROID_TOOLS" ] && echo -e "\033[0;31mFailed to find android-tools\033[0m" && exit + [ ! -d "$ANDROID_SDK_ROOT/ndk" ] && [ ! -d "$ANDROID_NDK_HOME" ] && echo -e "\033[0;31mFailed to find Android NDK\033[0m" && exit +fi + +if ! command -v flutter &>/dev/null; then + echo -e "\033[0;Could not find Flutter, please install flutter or add to path.\033[0m" + exit +fi + +flutter channel dev +flutter upgrade + +if [[ "$*" == *-android* ]]; then + flutter doctor --android-licenses +fi + +cd ../didkit + +if [[ "$*" == *-android* ]]; then + echo "Build didkit for Android" + make -C lib install-rustup-android + make -C lib ../target/test/aar.stamp +fi + +if [[ "$*" == *-ios* ]]; then + echo "Build didkit for iOS" + make -C lib install-rustup-ios + make -C lib ../target/test/ios.stamp +fi + +cargo build diff --git a/install_android_dependencies_for_debian_based_gnu_linux.sh b/install_android_dependencies_for_debian_based_gnu_linux.sh new file mode 100755 index 00000000..aaf2cf9a --- /dev/null +++ b/install_android_dependencies_for_debian_based_gnu_linux.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +## DEPENDENCIES +sudo apt update +sudo apt upgrade +sudo apt install -y git lib32z1 libssl-dev pkg-config build-essential curl cmake openjdk-11-jdk zip unzip +# if there are any error loading the above, re-run apt update and address them before continuing! +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +##CLONE SPRUCE REPOSITORIES +git clone https://github.com/spruceid/didkit +git clone https://github.com/spruceid/credible +git clone --recursive https://github.com/spruceid/ssi + +##FLUTTER +flutter channel stable + +##ANDROID SDK AND NDK +cd $HOME +wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip +unzip sdk-tools-linux-4333796.zip -d Android +rm sdk-tools-linux-4333796.zip +wget https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip +unzip commandlinetools-linux-6200805_latest.zip -d Android/cmdline-tools +rm commandlinetools-linux-6200805_latest.zip +echo 'export ANDROID_SDK_ROOT=$HOME/Android' >> $HOME/.bashrc +echo 'export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64' >> $HOME/.bashrc +echo 'export PATH=$ANDROID_SDK_ROOT/cmdline-tools/tools/bin:"$PATH"' >> $HOME/.bashrc +echo 'export PATH=$ANDROID_SDK_ROOT/cmdline-tools/tools/lib:"$PATH"' >> $HOME/.bashrc +echo 'export PATH=$ANDROID_SDK_ROOT/tools:"$PATH"' >> $HOME/.bashrc +echo 'export PATH=$JAVA_HOME/bin:"$PATH"' >> $HOME/.bashrc +. $HOME/.bashrc +sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "system-images;android-29;google_apis;x86" "system-images;android-29;google_apis;x86_64" "platform-tools" "platforms;android-29" "build-tools;29.0.3" "ndk;22.0.7026061" "cmdline-tools;latest" +sdkmanager --licenses + +##ANDROID EMULATOR +curl -s "https://get.sdkman.io" | bash +. .sdkman/bin/sdkman-init.sh +sdk install gradle 6.5.1 +echo "no" | avdmanager --verbose create avd --force --name "generic_10" --package "system-images;android-29;google_apis;x86" --tag "google_apis" --abi "x86" + +##BUILD DIDKIT +printf "You are now ready to run the following commands, but re-loading bash/OS may be necessary. +If rustup is inaccessible, reboot/re-load bash. +$ cd didkit +$ make -C lib install-rustup-android +$ make -C lib ../target/test/java.stamp +$ make -C lib ../target/test/aar.stamp +$ make -C lib ../target/test/flutter.stamp +$ cargo build\n\n" diff --git a/lib/common/router/routes.gr.dart b/lib/common/router/routes.gr.dart index 8e05ecda..f3e7e5c4 100644 --- a/lib/common/router/routes.gr.dart +++ b/lib/common/router/routes.gr.dart @@ -11,9 +11,9 @@ // ignore_for_file: type=lint import 'package:auto_route/auto_route.dart' as _i11; -import 'package:charge_wallet_sdk/charge_wallet_sdk.dart' as _i35; -import 'package:contacts_service/contacts_service.dart' as _i34; -import 'package:flutter/material.dart' as _i31; +import 'package:charge_wallet_sdk/charge_wallet_sdk.dart' as _i36; +import 'package:contacts_service/contacts_service.dart' as _i35; +import 'package:flutter/material.dart' as _i32; import '../../features/account/screens/account_screen.dart' as _i23; import '../../features/account/screens/connected_dapps.dart' as _i30; @@ -22,6 +22,7 @@ import '../../features/account/screens/profile.dart' as _i29; import '../../features/account/screens/protect_your_wallet.dart' as _i28; import '../../features/account/screens/settings.dart' as _i27; import '../../features/account/screens/show_mnemonic.dart' as _i24; +import '../../features/account/screens/verify_credential.dart' as _i31; import '../../features/account/screens/verify_mnemonic.dart' as _i25; import '../../features/home/screens/action_details.dart' as _i13; import '../../features/home/screens/home.dart' as _i12; @@ -44,18 +45,18 @@ import '../../features/wallet/screens/send_review.dart' as _i19; import '../../features/wallet/screens/send_success.dart' as _i20; import '../../features/wallet/screens/token.dart' as _i15; import '../../features/wallet/screens/wallet.dart' as _i14; -import '../../features/wallet/send_amount_arguments.dart' as _i36; -import '../../models/actions/wallet_action.dart' as _i33; -import '../../models/tokens/token.dart' as _i37; -import 'route_guards.dart' as _i32; +import '../../features/wallet/send_amount_arguments.dart' as _i37; +import '../../models/actions/wallet_action.dart' as _i34; +import '../../models/tokens/token.dart' as _i38; +import 'route_guards.dart' as _i33; class RootRouter extends _i11.RootStackRouter { RootRouter( - {_i31.GlobalKey<_i31.NavigatorState>? navigatorKey, + {_i32.GlobalKey<_i32.NavigatorState>? navigatorKey, required this.authGuard}) : super(navigatorKey); - final _i32.AuthGuard authGuard; + final _i33.AuthGuard authGuard; @override final Map pagesMap = { @@ -235,6 +236,13 @@ class RootRouter extends _i11.RootStackRouter { ConnectedDAppsRoute.name: (routeData) { return _i11.MaterialPageX( routeData: routeData, child: const _i30.ConnectedDAppsPage()); + }, + VerifyCredentialRoute.name: (routeData) { + final args = routeData.argsAs(); + return _i11.MaterialPageX( + routeData: routeData, + child: _i31.VerifyCredentialPage( + key: args.key, privateKeyForDID: args.privateKeyForDID)); } }; @@ -312,7 +320,9 @@ class RootRouter extends _i11.RootStackRouter { _i11.RouteConfig(ProfileRoute.name, path: 'profile-page', parent: AccountTab.name), _i11.RouteConfig(ConnectedDAppsRoute.name, - path: 'connected-dapps-page', parent: AccountTab.name) + path: 'connected-dapps-page', parent: AccountTab.name), + _i11.RouteConfig(VerifyCredentialRoute.name, + path: 'verify-credential-page', parent: AccountTab.name) ]) ]), _i11.RouteConfig('*#redirect', @@ -323,7 +333,7 @@ class RootRouter extends _i11.RootStackRouter { /// generated route for /// [_i1.SplashPage] class SplashRoute extends _i11.PageRouteInfo { - SplashRoute({_i31.Key? key, void Function(bool)? onLoginResult}) + SplashRoute({_i32.Key? key, void Function(bool)? onLoginResult}) : super(SplashRoute.name, path: '/', args: SplashRouteArgs(key: key, onLoginResult: onLoginResult)); @@ -334,7 +344,7 @@ class SplashRoute extends _i11.PageRouteInfo { class SplashRouteArgs { const SplashRouteArgs({this.key, this.onLoginResult}); - final _i31.Key? key; + final _i32.Key? key; final void Function(bool)? onLoginResult; @@ -391,7 +401,7 @@ class SignUpRoute extends _i11.PageRouteInfo { /// [_i7.VerifyPhoneNumberPage] class VerifyPhoneNumberRoute extends _i11.PageRouteInfo { - VerifyPhoneNumberRoute({String? verificationId, _i31.Key? key}) + VerifyPhoneNumberRoute({String? verificationId, _i32.Key? key}) : super(VerifyPhoneNumberRoute.name, path: '/verify-phone-number-page', args: VerifyPhoneNumberRouteArgs( @@ -405,7 +415,7 @@ class VerifyPhoneNumberRouteArgs { final String? verificationId; - final _i31.Key? key; + final _i32.Key? key; @override String toString() { @@ -428,7 +438,7 @@ class Webview extends _i11.PageRouteInfo { {required String url, required String title, void Function(String)? onPageStarted, - _i31.Key? key}) + _i32.Key? key}) : super(Webview.name, path: '/web-view-page', args: WebviewArgs( @@ -450,7 +460,7 @@ class WebviewArgs { final void Function(String)? onPageStarted; - final _i31.Key? key; + final _i32.Key? key; @override String toString() { @@ -515,13 +525,13 @@ class HomeRoute extends _i11.PageRouteInfo { /// [_i13.ActionDetailsPage] class ActionDetailsRoute extends _i11.PageRouteInfo { ActionDetailsRoute( - {_i31.Key? key, - required _i33.WalletAction action, - _i31.ImageProvider? image, + {_i32.Key? key, + required _i34.WalletAction action, + _i32.ImageProvider? image, required String displayName, String? accountAddress, required String symbol, - _i34.Contact? contact}) + _i35.Contact? contact}) : super(ActionDetailsRoute.name, path: 'action-details', args: ActionDetailsRouteArgs( @@ -546,11 +556,11 @@ class ActionDetailsRouteArgs { required this.symbol, this.contact}); - final _i31.Key? key; + final _i32.Key? key; - final _i33.WalletAction action; + final _i34.WalletAction action; - final _i31.ImageProvider? image; + final _i32.ImageProvider? image; final String displayName; @@ -558,7 +568,7 @@ class ActionDetailsRouteArgs { final String symbol; - final _i34.Contact? contact; + final _i35.Contact? contact; @override String toString() { @@ -577,7 +587,7 @@ class WalletRoute extends _i11.PageRouteInfo { /// generated route for /// [_i15.TokenPage] class TokenRoute extends _i11.PageRouteInfo { - TokenRoute({_i31.Key? key, required String tokenAddress}) + TokenRoute({_i32.Key? key, required String tokenAddress}) : super(TokenRoute.name, path: 'token-page', args: TokenRouteArgs(key: key, tokenAddress: tokenAddress)); @@ -588,7 +598,7 @@ class TokenRoute extends _i11.PageRouteInfo { class TokenRouteArgs { const TokenRouteArgs({this.key, required this.tokenAddress}); - final _i31.Key? key; + final _i32.Key? key; final String tokenAddress; @@ -601,7 +611,7 @@ class TokenRouteArgs { /// generated route for /// [_i16.CollectiblePage] class CollectibleRoute extends _i11.PageRouteInfo { - CollectibleRoute({_i31.Key? key, required _i35.Collectible collectible}) + CollectibleRoute({_i32.Key? key, required _i36.Collectible collectible}) : super(CollectibleRoute.name, path: 'collectible-page', args: CollectibleRouteArgs(key: key, collectible: collectible)); @@ -612,9 +622,9 @@ class CollectibleRoute extends _i11.PageRouteInfo { class CollectibleRouteArgs { const CollectibleRouteArgs({this.key, required this.collectible}); - final _i31.Key? key; + final _i32.Key? key; - final _i35.Collectible collectible; + final _i36.Collectible collectible; @override String toString() { @@ -625,7 +635,7 @@ class CollectibleRouteArgs { /// generated route for /// [_i17.ContactsPage] class ContactsRoute extends _i11.PageRouteInfo { - ContactsRoute({_i31.Key? key, _i36.SendFlowArguments? pageArgs}) + ContactsRoute({_i32.Key? key, _i37.SendFlowArguments? pageArgs}) : super(ContactsRoute.name, path: 'contacts-page', args: ContactsRouteArgs(key: key, pageArgs: pageArgs)); @@ -636,9 +646,9 @@ class ContactsRoute extends _i11.PageRouteInfo { class ContactsRouteArgs { const ContactsRouteArgs({this.key, this.pageArgs}); - final _i31.Key? key; + final _i32.Key? key; - final _i36.SendFlowArguments? pageArgs; + final _i37.SendFlowArguments? pageArgs; @override String toString() { @@ -649,7 +659,7 @@ class ContactsRouteArgs { /// generated route for /// [_i18.SendAmountPage] class SendAmountRoute extends _i11.PageRouteInfo { - SendAmountRoute({required _i36.SendFlowArguments pageArgs, _i31.Key? key}) + SendAmountRoute({required _i37.SendFlowArguments pageArgs, _i32.Key? key}) : super(SendAmountRoute.name, path: 'send-amount', args: SendAmountRouteArgs(pageArgs: pageArgs, key: key)); @@ -660,9 +670,9 @@ class SendAmountRoute extends _i11.PageRouteInfo { class SendAmountRouteArgs { const SendAmountRouteArgs({required this.pageArgs, this.key}); - final _i36.SendFlowArguments pageArgs; + final _i37.SendFlowArguments pageArgs; - final _i31.Key? key; + final _i32.Key? key; @override String toString() { @@ -673,7 +683,7 @@ class SendAmountRouteArgs { /// generated route for /// [_i19.SendReviewPage] class SendReviewRoute extends _i11.PageRouteInfo { - SendReviewRoute({required _i36.SendFlowArguments pageArgs, _i31.Key? key}) + SendReviewRoute({required _i37.SendFlowArguments pageArgs, _i32.Key? key}) : super(SendReviewRoute.name, path: 'send-review', args: SendReviewRouteArgs(pageArgs: pageArgs, key: key)); @@ -684,9 +694,9 @@ class SendReviewRoute extends _i11.PageRouteInfo { class SendReviewRouteArgs { const SendReviewRouteArgs({required this.pageArgs, this.key}); - final _i36.SendFlowArguments pageArgs; + final _i37.SendFlowArguments pageArgs; - final _i31.Key? key; + final _i32.Key? key; @override String toString() { @@ -697,7 +707,7 @@ class SendReviewRouteArgs { /// generated route for /// [_i20.SendSuccessPage] class SendSuccessRoute extends _i11.PageRouteInfo { - SendSuccessRoute({required _i36.SendFlowArguments pageArgs, _i31.Key? key}) + SendSuccessRoute({required _i37.SendFlowArguments pageArgs, _i32.Key? key}) : super(SendSuccessRoute.name, path: 'send-success', args: SendSuccessRouteArgs(pageArgs: pageArgs, key: key)); @@ -708,9 +718,9 @@ class SendSuccessRoute extends _i11.PageRouteInfo { class SendSuccessRouteArgs { const SendSuccessRouteArgs({required this.pageArgs, this.key}); - final _i36.SendFlowArguments pageArgs; + final _i37.SendFlowArguments pageArgs; - final _i31.Key? key; + final _i32.Key? key; @override String toString() { @@ -721,7 +731,7 @@ class SendSuccessRouteArgs { /// generated route for /// [_i21.SwapPage] class SwapRoute extends _i11.PageRouteInfo { - SwapRoute({_i31.Key? key, _i37.Token? primaryToken}) + SwapRoute({_i32.Key? key, _i38.Token? primaryToken}) : super(SwapRoute.name, path: '', args: SwapRouteArgs(key: key, primaryToken: primaryToken)); @@ -732,9 +742,9 @@ class SwapRoute extends _i11.PageRouteInfo { class SwapRouteArgs { const SwapRouteArgs({this.key, this.primaryToken}); - final _i31.Key? key; + final _i32.Key? key; - final _i37.Token? primaryToken; + final _i38.Token? primaryToken; @override String toString() { @@ -746,10 +756,10 @@ class SwapRouteArgs { /// [_i22.ReviewSwapPage] class ReviewSwapRoute extends _i11.PageRouteInfo { ReviewSwapRoute( - {_i35.Trade? rateInfo, - required _i35.Trade tradeInfo, - required _i35.TradeRequestBody swapRequestBody, - _i31.Key? key}) + {_i36.Trade? rateInfo, + required _i36.Trade tradeInfo, + required _i36.TradeRequestBody swapRequestBody, + _i32.Key? key}) : super(ReviewSwapRoute.name, path: 'review-swap-page', args: ReviewSwapRouteArgs( @@ -768,13 +778,13 @@ class ReviewSwapRouteArgs { required this.swapRequestBody, this.key}); - final _i35.Trade? rateInfo; + final _i36.Trade? rateInfo; - final _i35.Trade tradeInfo; + final _i36.Trade tradeInfo; - final _i35.TradeRequestBody swapRequestBody; + final _i36.TradeRequestBody swapRequestBody; - final _i31.Key? key; + final _i32.Key? key; @override String toString() { @@ -850,3 +860,29 @@ class ConnectedDAppsRoute extends _i11.PageRouteInfo { static const String name = 'ConnectedDAppsRoute'; } + +/// generated route for +/// [_i31.VerifyCredentialPage] +class VerifyCredentialRoute + extends _i11.PageRouteInfo { + VerifyCredentialRoute({_i32.Key? key, required String privateKeyForDID}) + : super(VerifyCredentialRoute.name, + path: 'verify-credential-page', + args: VerifyCredentialRouteArgs( + key: key, privateKeyForDID: privateKeyForDID)); + + static const String name = 'VerifyCredentialRoute'; +} + +class VerifyCredentialRouteArgs { + const VerifyCredentialRouteArgs({this.key, required this.privateKeyForDID}); + + final _i32.Key? key; + + final String privateKeyForDID; + + @override + String toString() { + return 'VerifyCredentialRouteArgs{key: $key, privateKeyForDID: $privateKeyForDID}'; + } +} diff --git a/lib/constants/strings.dart b/lib/constants/strings.dart index 89ffa34f..309b778f 100644 --- a/lib/constants/strings.dart +++ b/lib/constants/strings.dart @@ -3,4 +3,5 @@ class Strings { static const String appTitle = 'Fuse Wallet'; static const String appStoreId = '1491783654'; + static const defaultDIDMethod = "key"; } diff --git a/lib/features/account/providers.dart b/lib/features/account/providers.dart new file mode 100644 index 00000000..1e40c919 --- /dev/null +++ b/lib/features/account/providers.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fusecash/features/account/widget_models/verify_credential_page_model.dart'; + +final verifyCredentialPageModelProvider = Provider( + (_) => VerifyCredentialPageModel(privateKeyForDID: ""), +); diff --git a/lib/features/account/router/router.dart b/lib/features/account/router/router.dart index e1c21c32..1ce196d9 100644 --- a/lib/features/account/router/router.dart +++ b/lib/features/account/router/router.dart @@ -7,6 +7,7 @@ import 'package:fusecash/features/account/screens/protect_your_wallet.dart'; import 'package:fusecash/features/account/screens/settings.dart'; import 'package:fusecash/features/account/screens/show_mnemonic.dart'; import 'package:fusecash/features/account/screens/verify_mnemonic.dart'; +import 'package:fusecash/features/account/screens/verify_credential.dart'; const accountTab = AutoRoute( path: 'account', @@ -38,5 +39,8 @@ const accountTab = AutoRoute( AutoRoute( page: ConnectedDAppsPage, ), + AutoRoute( + page: VerifyCredentialPage, + ), ], ); diff --git a/lib/features/account/screens/account_screen.dart b/lib/features/account/screens/account_screen.dart index f3305ac8..8a2c6b69 100644 --- a/lib/features/account/screens/account_screen.dart +++ b/lib/features/account/screens/account_screen.dart @@ -10,6 +10,7 @@ import 'package:fusecash/common/router/routes.gr.dart'; import 'package:fusecash/constants/variables.dart'; import 'package:fusecash/features/account/widgets/avatar.dart'; import 'package:fusecash/features/account/widgets/menu_tile.dart'; +import 'package:fusecash/features/account/widgets/verify_credential_menu_tile.dart'; import 'package:fusecash/features/shared/bottom_sheets/deposit.dart'; import 'package:fusecash/features/shared/widgets/my_new_scaffold.dart'; import 'package:flutter_gen/gen_l10n/I10n.dart'; @@ -119,6 +120,11 @@ class _AccountPageState extends State { ); }, ), + viewModel.privateKeyForDID != null + ? VerifyCredentialMenuTile( + privateKeyForDID: viewModel.privateKeyForDID!, + ) + : const SizedBox(), ], ); }, diff --git a/lib/features/account/screens/verify_credential.dart b/lib/features/account/screens/verify_credential.dart new file mode 100644 index 00000000..e9325332 --- /dev/null +++ b/lib/features/account/screens/verify_credential.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fusecash/features/account/providers.dart'; +import 'package:fusecash/features/account/widget_models/verify_credential_page_model.dart'; +import 'package:fusecash/features/account/widgets/credential_text_field.dart'; +import 'package:fusecash/features/account/widgets/verify_credential_button.dart'; +import 'package:fusecash/features/shared/widgets/inner_page.dart'; + +class VerifyCredentialPage extends StatelessWidget { + final String privateKeyForDID; + + const VerifyCredentialPage({ + Key? key, + required this.privateKeyForDID, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + verifyCredentialPageModelProvider.overrideWithValue( + VerifyCredentialPageModel(privateKeyForDID: privateKeyForDID), + ), + ], + child: InnerScaffold( + title: "Verify Credential", + padding: const EdgeInsets.symmetric(horizontal: 24), + body: ListView( + children: const [ + CredentialTextField(), + SizedBox(height: 60), + VerifyCredentialButton(), + ], + ), + ), + ); + } +} diff --git a/lib/features/account/widget_models/verify_credential_page_model.dart b/lib/features/account/widget_models/verify_credential_page_model.dart new file mode 100644 index 00000000..efcfc612 --- /dev/null +++ b/lib/features/account/widget_models/verify_credential_page_model.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +class VerifyCredentialPageModel { + final String privateKeyForDID; + + VerifyCredentialPageModel({required this.privateKeyForDID}); + + final credentialTextController = TextEditingController(); +} diff --git a/lib/features/account/widgets/credential_text_field.dart b/lib/features/account/widgets/credential_text_field.dart new file mode 100644 index 00000000..2a780fc7 --- /dev/null +++ b/lib/features/account/widgets/credential_text_field.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fusecash/features/account/providers.dart'; + +class CredentialTextField extends ConsumerWidget { + const CredentialTextField({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final verifyCredentialPageModel = + ref.watch(verifyCredentialPageModelProvider); + final textController = verifyCredentialPageModel.credentialTextController; + return TextField( + controller: textController, + decoration: const InputDecoration( + hintText: "Insert the Verifiable Credential in JSON format here", + hintStyle: TextStyle(color: Colors.grey, fontSize: 12), + ), + maxLines: 10, + ); + } +} diff --git a/lib/features/account/widgets/verify_credential_button.dart b/lib/features/account/widgets/verify_credential_button.dart new file mode 100644 index 00000000..f921d928 --- /dev/null +++ b/lib/features/account/widgets/verify_credential_button.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fusecash/common/di/di.dart'; +import 'package:fusecash/features/account/providers.dart'; +import 'package:fusecash/models/verifiable_credential/verification_result.dart'; +import 'package:fusecash/utils/alerts/alerts.dart'; +import 'package:fusecash/utils/alerts/alerts_model.dart'; +import 'package:fusecash/utils/did/did_service.dart'; + +class VerifyCredentialButton extends ConsumerWidget { + const VerifyCredentialButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final verifyCredentialPageModel = + ref.watch(verifyCredentialPageModelProvider); + return ValueListenableBuilder( + valueListenable: verifyCredentialPageModel.credentialTextController, + builder: (_, value, __) { + final credential = value.text; + final primaryColor = Theme.of(context).colorScheme.primary; + final buttonColor = + credential.isEmpty ? primaryColor.withOpacity(0.5) : primaryColor; + return ElevatedButton( + onPressed: () { + _onPressed( + context: context, + credential: credential, + privateKeyForDID: verifyCredentialPageModel.privateKeyForDID, + ); + }, + style: _getStyle(buttonColor), + child: const Text( + "Verify Credential", + style: TextStyle(color: Colors.white, fontSize: 18), + ), + ); + }, + ); + } + + void _onPressed({ + required BuildContext context, + required String credential, + required String privateKeyForDID, + }) { + final alerts = getIt(); + + try { + final didService = DIDService(privateKey: privateKeyForDID); + final result = didService.verifyCredential(credential); + + final resultAsMap = jsonDecode(result); + final verificationResult = VerificationResult.fromJson(resultAsMap); + + _showAlertAccordingToTheResult(context, verificationResult); + } on Exception catch (exception) { + final alertMessage = + "Failed to verify credential. ${exception.toString()}"; + alerts.setAlert(context, alertMessage, type: AlertsTypeEnum.ERROR); + } + } + + void _showAlertAccordingToTheResult( + BuildContext context, + VerificationResult result, + ) { + final warnings = result.warnings; + final errors = result.errors; + + final alerts = getIt(); + + if (errors.isEmpty) { + String alertMessage = "Credential has been verified successfully."; + if (warnings.isNotEmpty) { + alertMessage += " $warnings"; + } + alerts.setAlert(context, alertMessage, type: AlertsTypeEnum.SUCCESS); + } else { + final alertMessage = "Failed to verify credential. $errors"; + alerts.setAlert(context, alertMessage, type: AlertsTypeEnum.ERROR); + } + } + + ButtonStyle _getStyle(Color buttonColor) { + return ElevatedButton.styleFrom( + primary: buttonColor, + elevation: 0.0, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + ); + } +} diff --git a/lib/features/account/widgets/verify_credential_menu_tile.dart b/lib/features/account/widgets/verify_credential_menu_tile.dart new file mode 100644 index 00000000..cfdb2c9e --- /dev/null +++ b/lib/features/account/widgets/verify_credential_menu_tile.dart @@ -0,0 +1,26 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:fusecash/common/router/routes.dart'; +import 'package:fusecash/features/account/widgets/menu_tile.dart'; + +class VerifyCredentialMenuTile extends StatelessWidget { + final String privateKeyForDID; + + const VerifyCredentialMenuTile({ + Key? key, + required this.privateKeyForDID, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return MenuTile( + label: "Verify Credential", + menuIcon: 'legal_icon.svg', + onTap: () { + context.router.push( + VerifyCredentialRoute(privateKeyForDID: privateKeyForDID), + ); + }, + ); + } +} diff --git a/lib/features/screens/splash_screen.dart b/lib/features/screens/splash_screen.dart index 449c3443..d1419927 100644 --- a/lib/features/screens/splash_screen.dart +++ b/lib/features/screens/splash_screen.dart @@ -16,6 +16,7 @@ import 'package:fusecash/utils/biometric_local_auth.dart'; class SplashPage extends StatefulWidget { final void Function(bool isLoggedIn)? onLoginResult; + const SplashPage({ Key? key, this.onLoginResult, @@ -27,11 +28,37 @@ class SplashPage extends StatefulWidget { class _SplashPageState extends State { late Flushbar flush; + void onInit(Store store) async { UserState userState = store.state.userState; String privateKey = userState.privateKey; String jwtToken = userState.jwtToken; bool isLoggedOut = userState.isLoggedOut; + + final mnemonic = userState.mnemonic; + + final did = userState.did; + final didExists = did != null && did.isNotEmpty; + + if (didExists && userState.userInfoCredential == null) { + final privateKeyForDID = userState.privateKeyForDID; + + // If did exists, the private must exist too. + store.dispatch( + issueUserInfoCredentialCall( + did: did, + privateKeyForDID: privateKeyForDID!, + ), + ); + } + + if (!didExists && mnemonic.isNotEmpty) { + final mnemonic = userState.mnemonic.join(" "); + store.dispatch( + generateDIDCall(mnemonic: mnemonic), + ); + } + if (privateKey.isEmpty || jwtToken.isEmpty || isLoggedOut) { context.router.replaceAll([const OnBoardingRoute()]); widget.onLoginResult?.call(false); diff --git a/lib/models/user_state.dart b/lib/models/user_state.dart index 098d0222..3c93fcef 100644 --- a/lib/models/user_state.dart +++ b/lib/models/user_state.dart @@ -54,6 +54,9 @@ class UserState with _$UserState { @JsonKey(fromJson: localeFromJson, toJson: localeToJson) Locale? locale, @JsonKey(ignore: true) @Default([]) List contacts, @JsonKey(ignore: true) PhoneAuthCredential? credentials, + String? did, + String? privateKeyForDID, + String? userInfoCredential, }) = _UserState; factory UserState.initial() => UserState( diff --git a/lib/models/user_state.freezed.dart b/lib/models/user_state.freezed.dart index 2eb35715..89cc56cd 100644 --- a/lib/models/user_state.freezed.dart +++ b/lib/models/user_state.freezed.dart @@ -57,6 +57,9 @@ mixin _$UserState { List get contacts => throw _privateConstructorUsedError; @JsonKey(ignore: true) PhoneAuthCredential? get credentials => throw _privateConstructorUsedError; + String? get did => throw _privateConstructorUsedError; + String? get privateKeyForDID => throw _privateConstructorUsedError; + String? get userInfoCredential => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -100,7 +103,10 @@ abstract class $UserStateCopyWith<$Res> { BiometricAuth authType, @JsonKey(fromJson: localeFromJson, toJson: localeToJson) Locale? locale, @JsonKey(ignore: true) List contacts, - @JsonKey(ignore: true) PhoneAuthCredential? credentials}); + @JsonKey(ignore: true) PhoneAuthCredential? credentials, + String? did, + String? privateKeyForDID, + String? userInfoCredential}); $WalletModulesCopyWith<$Res>? get walletModules; } @@ -147,6 +153,9 @@ class _$UserStateCopyWithImpl<$Res> implements $UserStateCopyWith<$Res> { Object? locale = freezed, Object? contacts = freezed, Object? credentials = freezed, + Object? did = freezed, + Object? privateKeyForDID = freezed, + Object? userInfoCredential = freezed, }) { return _then(_value.copyWith( wcURI: wcURI == freezed @@ -277,6 +286,18 @@ class _$UserStateCopyWithImpl<$Res> implements $UserStateCopyWith<$Res> { ? _value.credentials : credentials // ignore: cast_nullable_to_non_nullable as PhoneAuthCredential?, + did: did == freezed + ? _value.did + : did // ignore: cast_nullable_to_non_nullable + as String?, + privateKeyForDID: privateKeyForDID == freezed + ? _value.privateKeyForDID + : privateKeyForDID // ignore: cast_nullable_to_non_nullable + as String?, + userInfoCredential: userInfoCredential == freezed + ? _value.userInfoCredential + : userInfoCredential // ignore: cast_nullable_to_non_nullable + as String?, )); } @@ -330,7 +351,10 @@ abstract class _$$_UserStateCopyWith<$Res> implements $UserStateCopyWith<$Res> { BiometricAuth authType, @JsonKey(fromJson: localeFromJson, toJson: localeToJson) Locale? locale, @JsonKey(ignore: true) List contacts, - @JsonKey(ignore: true) PhoneAuthCredential? credentials}); + @JsonKey(ignore: true) PhoneAuthCredential? credentials, + String? did, + String? privateKeyForDID, + String? userInfoCredential}); @override $WalletModulesCopyWith<$Res>? get walletModules; @@ -380,6 +404,9 @@ class __$$_UserStateCopyWithImpl<$Res> extends _$UserStateCopyWithImpl<$Res> Object? locale = freezed, Object? contacts = freezed, Object? credentials = freezed, + Object? did = freezed, + Object? privateKeyForDID = freezed, + Object? userInfoCredential = freezed, }) { return _then(_$_UserState( wcURI: wcURI == freezed @@ -510,6 +537,18 @@ class __$$_UserStateCopyWithImpl<$Res> extends _$UserStateCopyWithImpl<$Res> ? _value.credentials : credentials // ignore: cast_nullable_to_non_nullable as PhoneAuthCredential?, + did: did == freezed + ? _value.did + : did // ignore: cast_nullable_to_non_nullable + as String?, + privateKeyForDID: privateKeyForDID == freezed + ? _value.privateKeyForDID + : privateKeyForDID // ignore: cast_nullable_to_non_nullable + as String?, + userInfoCredential: userInfoCredential == freezed + ? _value.userInfoCredential + : userInfoCredential // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -549,7 +588,10 @@ class _$_UserState extends _UserState with DiagnosticableTreeMixin { this.authType = BiometricAuth.none, @JsonKey(fromJson: localeFromJson, toJson: localeToJson) this.locale, @JsonKey(ignore: true) this.contacts = const [], - @JsonKey(ignore: true) this.credentials}) + @JsonKey(ignore: true) this.credentials, + this.did, + this.privateKeyForDID, + this.userInfoCredential}) : super._(); factory _$_UserState.fromJson(Map json) => @@ -646,10 +688,16 @@ class _$_UserState extends _UserState with DiagnosticableTreeMixin { @override @JsonKey(ignore: true) final PhoneAuthCredential? credentials; + @override + final String? did; + @override + final String? privateKeyForDID; + @override + final String? userInfoCredential; @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'UserState(wcURI: $wcURI, contractVersion: $contractVersion, walletModules: $walletModules, installedAt: $installedAt, isContactsSynced: $isContactsSynced, isLoggedOut: $isLoggedOut, backup: $backup, scrollToTop: $scrollToTop, walletAddress: $walletAddress, networks: $networks, mnemonic: $mnemonic, privateKey: $privateKey, pincode: $pincode, accountAddress: $accountAddress, countryCode: $countryCode, phoneNumber: $phoneNumber, warnSendDialogShowed: $warnSendDialogShowed, isoCode: $isoCode, jwtToken: $jwtToken, displayName: $displayName, avatarUrl: $avatarUrl, email: $email, verificationId: $verificationId, identifier: $identifier, syncedContacts: $syncedContacts, reverseContacts: $reverseContacts, currency: $currency, hasUpgrade: $hasUpgrade, authType: $authType, locale: $locale, contacts: $contacts, credentials: $credentials)'; + return 'UserState(wcURI: $wcURI, contractVersion: $contractVersion, walletModules: $walletModules, installedAt: $installedAt, isContactsSynced: $isContactsSynced, isLoggedOut: $isLoggedOut, backup: $backup, scrollToTop: $scrollToTop, walletAddress: $walletAddress, networks: $networks, mnemonic: $mnemonic, privateKey: $privateKey, pincode: $pincode, accountAddress: $accountAddress, countryCode: $countryCode, phoneNumber: $phoneNumber, warnSendDialogShowed: $warnSendDialogShowed, isoCode: $isoCode, jwtToken: $jwtToken, displayName: $displayName, avatarUrl: $avatarUrl, email: $email, verificationId: $verificationId, identifier: $identifier, syncedContacts: $syncedContacts, reverseContacts: $reverseContacts, currency: $currency, hasUpgrade: $hasUpgrade, authType: $authType, locale: $locale, contacts: $contacts, credentials: $credentials, did: $did, privateKeyForDID: $privateKeyForDID, userInfoCredential: $userInfoCredential)'; } @override @@ -688,7 +736,10 @@ class _$_UserState extends _UserState with DiagnosticableTreeMixin { ..add(DiagnosticsProperty('authType', authType)) ..add(DiagnosticsProperty('locale', locale)) ..add(DiagnosticsProperty('contacts', contacts)) - ..add(DiagnosticsProperty('credentials', credentials)); + ..add(DiagnosticsProperty('credentials', credentials)) + ..add(DiagnosticsProperty('did', did)) + ..add(DiagnosticsProperty('privateKeyForDID', privateKeyForDID)) + ..add(DiagnosticsProperty('userInfoCredential', userInfoCredential)); } @override @@ -746,7 +797,12 @@ class _$_UserState extends _UserState with DiagnosticableTreeMixin { const DeepCollectionEquality().equals(other.locale, locale) && const DeepCollectionEquality().equals(other.contacts, contacts) && const DeepCollectionEquality() - .equals(other.credentials, credentials)); + .equals(other.credentials, credentials) && + const DeepCollectionEquality().equals(other.did, did) && + const DeepCollectionEquality() + .equals(other.privateKeyForDID, privateKeyForDID) && + const DeepCollectionEquality() + .equals(other.userInfoCredential, userInfoCredential)); } @JsonKey(ignore: true) @@ -784,7 +840,10 @@ class _$_UserState extends _UserState with DiagnosticableTreeMixin { const DeepCollectionEquality().hash(authType), const DeepCollectionEquality().hash(locale), const DeepCollectionEquality().hash(contacts), - const DeepCollectionEquality().hash(credentials) + const DeepCollectionEquality().hash(credentials), + const DeepCollectionEquality().hash(did), + const DeepCollectionEquality().hash(privateKeyForDID), + const DeepCollectionEquality().hash(userInfoCredential) ]); @JsonKey(ignore: true) @@ -838,7 +897,10 @@ abstract class _UserState extends UserState { @JsonKey(ignore: true) final List contacts, @JsonKey(ignore: true) - final PhoneAuthCredential? credentials}) = _$_UserState; + final PhoneAuthCredential? credentials, + final String? did, + final String? privateKeyForDID, + final String? userInfoCredential}) = _$_UserState; _UserState._() : super._(); factory _UserState.fromJson(Map json) = @@ -914,6 +976,12 @@ abstract class _UserState extends UserState { @JsonKey(ignore: true) PhoneAuthCredential? get credentials; @override + String? get did; + @override + String? get privateKeyForDID; + @override + String? get userInfoCredential; + @override @JsonKey(ignore: true) _$$_UserStateCopyWith<_$_UserState> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/user_state.g.dart b/lib/models/user_state.g.dart index 581031b9..05ccf006 100644 --- a/lib/models/user_state.g.dart +++ b/lib/models/user_state.g.dart @@ -53,6 +53,9 @@ _$_UserState _$$_UserStateFromJson(Map json) => _$_UserState( authType: $enumDecodeNullable(_$BiometricAuthEnumMap, json['authType']) ?? BiometricAuth.none, locale: localeFromJson(json['locale'] as Map), + did: json['did'] as String?, + privateKeyForDID: json['privateKeyForDID'] as String?, + userInfoCredential: json['userInfoCredential'] as String?, ); Map _$$_UserStateToJson(_$_UserState instance) => @@ -85,6 +88,9 @@ Map _$$_UserStateToJson(_$_UserState instance) => 'currency': instance.currency, 'authType': _$BiometricAuthEnumMap[instance.authType]!, 'locale': localeToJson(instance.locale), + 'did': instance.did, + 'privateKeyForDID': instance.privateKeyForDID, + 'userInfoCredential': instance.userInfoCredential, }; const _$BiometricAuthEnumMap = { diff --git a/lib/models/verifiable_credential/credential_subject.dart b/lib/models/verifiable_credential/credential_subject.dart new file mode 100644 index 00000000..1eb881a7 --- /dev/null +++ b/lib/models/verifiable_credential/credential_subject.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'credential_subject.g.dart'; + +@JsonSerializable() +class CredentialSubject { + final String id; + final String type; + + const CredentialSubject({required this.id, required this.type}); + + factory CredentialSubject.fromJson(Map json) => + _$CredentialSubjectFromJson(json); + + Map toJson() => _$CredentialSubjectToJson(this); +} diff --git a/lib/models/verifiable_credential/credential_subject.g.dart b/lib/models/verifiable_credential/credential_subject.g.dart new file mode 100644 index 00000000..a3213bbe --- /dev/null +++ b/lib/models/verifiable_credential/credential_subject.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'credential_subject.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CredentialSubject _$CredentialSubjectFromJson(Map json) => + CredentialSubject( + id: json['id'] as String, + type: json['type'] as String, + ); + +Map _$CredentialSubjectToJson(CredentialSubject instance) => + { + 'id': instance.id, + 'type': instance.type, + }; diff --git a/lib/models/verifiable_credential/issue_credential_options.dart b/lib/models/verifiable_credential/issue_credential_options.dart new file mode 100644 index 00000000..efa870b0 --- /dev/null +++ b/lib/models/verifiable_credential/issue_credential_options.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'issue_credential_options.g.dart'; + +@JsonSerializable() +class IssueCredentialOptions { + final String proofPurpose; + final String verificationMethod; + + const IssueCredentialOptions({ + required this.proofPurpose, + required this.verificationMethod, + }); + + Map toJson() => _$IssueCredentialOptionsToJson(this); +} diff --git a/lib/models/verifiable_credential/issue_credential_options.g.dart b/lib/models/verifiable_credential/issue_credential_options.g.dart new file mode 100644 index 00000000..a78cc507 --- /dev/null +++ b/lib/models/verifiable_credential/issue_credential_options.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'issue_credential_options.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +IssueCredentialOptions _$IssueCredentialOptionsFromJson( + Map json) => + IssueCredentialOptions( + proofPurpose: json['proofPurpose'] as String, + verificationMethod: json['verificationMethod'] as String, + ); + +Map _$IssueCredentialOptionsToJson( + IssueCredentialOptions instance) => + { + 'proofPurpose': instance.proofPurpose, + 'verificationMethod': instance.verificationMethod, + }; diff --git a/lib/models/verifiable_credential/user_info_credential.dart b/lib/models/verifiable_credential/user_info_credential.dart new file mode 100644 index 00000000..4d297b94 --- /dev/null +++ b/lib/models/verifiable_credential/user_info_credential.dart @@ -0,0 +1,58 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:fusecash/models/verifiable_credential/user_info_credential_subject.dart'; + +part 'user_info_credential.g.dart'; + +@JsonSerializable() +class UserInfoCredential { + @JsonKey(name: "@context", defaultValue: _context) + final List context; + + final String id; + + @JsonKey(defaultValue: _type) + final List type; + + final String issuer; + final String issuanceDate; + + @JsonKey(name: "credentialSubject") + final UserInfoCredentialSubject userInfoCredentialSubject; + + const UserInfoCredential({ + this.context = _context, + required this.id, + this.type = _type, + required this.issuer, + required this.issuanceDate, + required this.userInfoCredentialSubject, + }); + + factory UserInfoCredential.fromJson(Map json) => + _$UserInfoCredentialFromJson(json); + + Map toJson() => _$UserInfoCredentialToJson(this); + + static const _context = [ + "https://www.w3.org/2018/credentials/v1", + { + 'name': 'https://schema.org/name', + 'description': 'https://schema.org/description', + 'SelfIssued': { + '@context': { + '@protected': true, + '@version': 1.1, + 'id': '@id', + 'schema': 'https://schema.org/', + 'telephone': 'schema:telephone', + 'type': '@type' + }, + // TODO: Create a README file for Fuse + // TODO: and use it instead of the one below. + '@id': 'https://github.com/TalaoDAO/context/blob/main/README.md' + } + }, + ]; + + static const _type = ["VerifiableCredential", "SelfIssued"]; +} diff --git a/lib/models/verifiable_credential/user_info_credential.g.dart b/lib/models/verifiable_credential/user_info_credential.g.dart new file mode 100644 index 00000000..4eeaf3f0 --- /dev/null +++ b/lib/models/verifiable_credential/user_info_credential.g.dart @@ -0,0 +1,48 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_info_credential.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserInfoCredential _$UserInfoCredentialFromJson(Map json) => + UserInfoCredential( + context: json['@context'] as List? ?? + [ + 'https://www.w3.org/2018/credentials/v1', + { + 'name': 'https://schema.org/name', + 'description': 'https://schema.org/description', + 'SelfIssued': { + '@context': { + '@protected': true, + '@version': 1.1, + 'id': '@id', + 'schema': 'https://schema.org/', + 'telephone': 'schema:telephone', + 'type': '@type' + }, + '@id': 'https://github.com/TalaoDAO/context/blob/main/README.md' + } + } + ], + id: json['id'] as String, + type: + (json['type'] as List?)?.map((e) => e as String).toList() ?? + ['VerifiableCredential', 'SelfIssued'], + issuer: json['issuer'] as String, + issuanceDate: json['issuanceDate'] as String, + userInfoCredentialSubject: UserInfoCredentialSubject.fromJson( + json['credentialSubject'] as Map), + ); + +Map _$UserInfoCredentialToJson(UserInfoCredential instance) => + { + '@context': instance.context, + 'id': instance.id, + 'type': instance.type, + 'issuer': instance.issuer, + 'issuanceDate': instance.issuanceDate, + 'credentialSubject': instance.userInfoCredentialSubject.toJson(), + }; diff --git a/lib/models/verifiable_credential/user_info_credential_subject.dart b/lib/models/verifiable_credential/user_info_credential_subject.dart new file mode 100644 index 00000000..f7580c24 --- /dev/null +++ b/lib/models/verifiable_credential/user_info_credential_subject.dart @@ -0,0 +1,25 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:fusecash/models/verifiable_credential/credential_subject.dart'; + +part 'user_info_credential_subject.g.dart'; + +@JsonSerializable() +class UserInfoCredentialSubject extends CredentialSubject { + final String name; + + @JsonKey(name: "telephone") + final String phoneNumber; + + const UserInfoCredentialSubject({ + required String id, + String type = "SelfIssued", + required this.name, + required this.phoneNumber, + }) : super(id: id, type: type); + + factory UserInfoCredentialSubject.fromJson(Map json) => + _$UserInfoCredentialSubjectFromJson(json); + + @override + Map toJson() => _$UserInfoCredentialSubjectToJson(this); +} diff --git a/lib/models/verifiable_credential/user_info_credential_subject.g.dart b/lib/models/verifiable_credential/user_info_credential_subject.g.dart new file mode 100644 index 00000000..8739682d --- /dev/null +++ b/lib/models/verifiable_credential/user_info_credential_subject.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_info_credential_subject.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserInfoCredentialSubject _$UserInfoCredentialSubjectFromJson( + Map json) => + UserInfoCredentialSubject( + id: json['id'] as String, + type: json['type'] as String? ?? "SelfIssued", + name: json['name'] as String, + phoneNumber: json['telephone'] as String, + ); + +Map _$UserInfoCredentialSubjectToJson( + UserInfoCredentialSubject instance) => + { + 'id': instance.id, + 'type': instance.type, + 'name': instance.name, + 'telephone': instance.phoneNumber, + }; diff --git a/lib/models/verifiable_credential/verification_result.dart b/lib/models/verifiable_credential/verification_result.dart new file mode 100644 index 00000000..9f63235b --- /dev/null +++ b/lib/models/verifiable_credential/verification_result.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'verification_result.g.dart'; + +@JsonSerializable() +class VerificationResult { + final List warnings; + final List errors; + + const VerificationResult({required this.warnings, required this.errors}); + + factory VerificationResult.fromJson(Map json) => + _$VerificationResultFromJson(json); +} diff --git a/lib/models/verifiable_credential/verification_result.g.dart b/lib/models/verifiable_credential/verification_result.g.dart new file mode 100644 index 00000000..58ccfb52 --- /dev/null +++ b/lib/models/verifiable_credential/verification_result.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'verification_result.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +VerificationResult _$VerificationResultFromJson(Map json) => + VerificationResult( + warnings: + (json['warnings'] as List).map((e) => e as String).toList(), + errors: + (json['errors'] as List).map((e) => e as String).toList(), + ); + +Map _$VerificationResultToJson(VerificationResult instance) => + { + 'warnings': instance.warnings, + 'errors': instance.errors, + }; diff --git a/lib/models/verifiable_credential/verify_credential_options.dart b/lib/models/verifiable_credential/verify_credential_options.dart new file mode 100644 index 00000000..6915b0ae --- /dev/null +++ b/lib/models/verifiable_credential/verify_credential_options.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'verify_credential_options.g.dart'; + +@JsonSerializable() +class VerifyCredentialOptions { + final String proofPurpose; + + const VerifyCredentialOptions({required this.proofPurpose}); + + Map toJson() => _$VerifyCredentialOptionsToJson(this); +} diff --git a/lib/models/verifiable_credential/verify_credential_options.g.dart b/lib/models/verifiable_credential/verify_credential_options.g.dart new file mode 100644 index 00000000..96bb70cb --- /dev/null +++ b/lib/models/verifiable_credential/verify_credential_options.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'verify_credential_options.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +VerifyCredentialOptions _$VerifyCredentialOptionsFromJson( + Map json) => + VerifyCredentialOptions( + proofPurpose: json['proofPurpose'] as String, + ); + +Map _$VerifyCredentialOptionsToJson( + VerifyCredentialOptions instance) => + { + 'proofPurpose': instance.proofPurpose, + }; diff --git a/lib/redux/actions/user_actions.dart b/lib/redux/actions/user_actions.dart index 7b088268..052b7a81 100644 --- a/lib/redux/actions/user_actions.dart +++ b/lib/redux/actions/user_actions.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -11,6 +12,10 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter_udid/flutter_udid.dart'; +import 'package:fusecash/models/verifiable_credential/user_info_credential.dart'; +import 'package:fusecash/models/verifiable_credential/verification_result.dart'; +import 'package:fusecash/utils/did/did_service.dart'; +import 'package:fusecash/utils/did/private_key_generation.dart'; import 'package:image_picker/image_picker.dart'; import 'package:phone_number/phone_number.dart'; import 'package:redux/redux.dart'; @@ -32,11 +37,13 @@ import 'package:fusecash/utils/phone.dart'; class SetWalletConnectURI { final String wcURI; + SetWalletConnectURI(this.wcURI); } class ScrollToTop { final bool value; + ScrollToTop( this.value, ); @@ -44,6 +51,7 @@ class ScrollToTop { class ToggleUpgrade { final bool value; + ToggleUpgrade({ required this.value, }); @@ -51,16 +59,19 @@ class ToggleUpgrade { class UpdateCurrency { final String currency; + UpdateCurrency({required this.currency}); } class UpdateLocale { final Locale locale; + UpdateLocale({required this.locale}); } class WarnSendDialogShowed { final bool value; + WarnSendDialogShowed( this.value, ); @@ -68,6 +79,7 @@ class WarnSendDialogShowed { class SetSecurityType { BiometricAuth biometricAuth; + SetSecurityType({required this.biometricAuth}); } @@ -75,6 +87,7 @@ class CreateLocalAccountSuccess { final List mnemonic; final String privateKey; final String accountAddress; + CreateLocalAccountSuccess( this.mnemonic, this.privateKey, @@ -82,6 +95,22 @@ class CreateLocalAccountSuccess { ); } +class GenerateDIDSuccess { + final String did; + + /// The private key used for DID features. + final String privateKeyForDID; + + const GenerateDIDSuccess(this.did, this.privateKeyForDID); +} + +class IssueUserInfoCredentialSuccess { + /// The JSON representation of the issued [UserInfoCredential]. + final String userInfoCredential; + + const IssueUserInfoCredentialSuccess({required this.userInfoCredential}); +} + class ReLogin { ReLogin(); } @@ -91,6 +120,7 @@ class LoginRequestSuccess { final String phoneNumber; final String? displayName; final String? email; + LoginRequestSuccess({ required this.countryCode, required this.phoneNumber, @@ -105,12 +135,14 @@ class LogoutRequestSuccess { class LoginVerifySuccess { final String jwtToken; + LoginVerifySuccess(this.jwtToken); } class SyncContactsProgress { List contacts; List> newContacts; + SyncContactsProgress(this.contacts, this.newContacts); } @@ -120,21 +152,25 @@ class SyncContactsRejected { class SaveContacts { List contacts; + SaveContacts(this.contacts); } class SetPincodeSuccess { String pincode; + SetPincodeSuccess(this.pincode); } class SetDisplayName { String displayName; + SetDisplayName(this.displayName); } class SetUserAvatar { String avatarUrl; + SetUserAvatar(this.avatarUrl); } @@ -148,21 +184,25 @@ class BackupSuccess { class SetCredentials { PhoneAuthCredential? credentials; + SetCredentials(this.credentials); } class SetVerificationId { String verificationId; + SetVerificationId(this.verificationId); } class JustInstalled { final DateTime installedAt; + JustInstalled(this.installedAt); } class DeviceIdSuccess { final String identifier; + DeviceIdSuccess(this.identifier); } @@ -284,14 +324,22 @@ ThunkAction restoreWalletCall( log.info('restore wallet'); log.info('mnemonic: $mnemonic'); log.info('compute pk'); + + final mnemonicAsString = mnemonic.join(' '); + String privateKey = await compute( Web3.privateKeyFromMnemonic, - mnemonic.join(' '), + mnemonicAsString, ); Credentials credentials = EthPrivateKey.fromHex(privateKey); EthereumAddress accountAddress = await credentials.extractAddress(); log.info('privateKey: $privateKey'); log.info('accountAddress: ${accountAddress.toString()}'); + + store.dispatch( + generateDIDCall(mnemonic: mnemonicAsString), + ); + store.dispatch( CreateLocalAccountSuccess( mnemonic, @@ -342,6 +390,11 @@ ThunkAction createLocalAccountCall( EthereumAddress accountAddress = await credentials.extractAddress(); log.info('privateKey: $privateKey'); log.info('accountAddress: ${accountAddress.toString()}'); + + store.dispatch( + generateDIDCall(mnemonic: mnemonic), + ); + store.dispatch( CreateLocalAccountSuccess( mnemonic.split(' '), @@ -561,6 +614,86 @@ ThunkAction loadContacts() { }; } +ThunkAction generateDIDCall({required String mnemonic}) { + assert(mnemonic.isNotEmpty, "Mnemonic must not be empty"); + return (Store store) async { + try { + final privateKeyGeneration = PrivateKeyGeneration(); + final privateKeyToGenerateDID = + await privateKeyGeneration.generatePrivateKey(mnemonic); + + final didService = DIDService(privateKey: privateKeyToGenerateDID); + final did = didService.generateDID(); + + debugPrint("did: $did"); + + store.dispatch( + issueUserInfoCredentialCall( + did: did, + privateKeyForDID: privateKeyToGenerateDID, + ), + ); + + final generateDIDSuccess = + GenerateDIDSuccess(did, privateKeyToGenerateDID); + store.dispatch(generateDIDSuccess); + } catch (exception, stackTrace) { + const errorMessage = "An error occurred while generating DID."; + log.error( + errorMessage, + error: exception, + stackTrace: stackTrace, + ); + Crashlytics.recordError( + Exception("$errorMessage ${exception.toString()}"), + stackTrace, + reason: errorMessage, + ); + } + }; +} + +ThunkAction issueUserInfoCredentialCall({ + required String did, + required String privateKeyForDID, +}) { + return (Store store) async { + final userState = store.state.userState; + final didService = DIDService(privateKey: privateKeyForDID); + + final userInfoCredential = didService.issueUserInfoCredential( + did: did, + name: userState.displayName, + phoneNumber: userState.phoneNumber, + ); + + debugPrint("userInfoCredential: $userInfoCredential"); + + final verificationResultInJson = + didService.verifyCredential(userInfoCredential); + + final verificationResultAsMap = jsonDecode(verificationResultInJson); + final verificationResult = + VerificationResult.fromJson(verificationResultAsMap); + + final warnings = verificationResult.warnings; + final errors = verificationResult.errors; + + if (warnings.isNotEmpty) { + log.warn("Warnings produced while verifying the credential: $warnings"); + } + + if (errors.isNotEmpty) { + log.error("Failed to verify credential. $errors"); + return; + } + + final issueUserInfoCredentialSuccess = + IssueUserInfoCredentialSuccess(userInfoCredential: userInfoCredential); + store.dispatch(issueUserInfoCredentialSuccess); + }; +} + ThunkAction web3Init({ WalletModules? modules, }) { diff --git a/lib/redux/reducers/user_reducer.dart b/lib/redux/reducers/user_reducer.dart index 160da60f..1553063a 100644 --- a/lib/redux/reducers/user_reducer.dart +++ b/lib/redux/reducers/user_reducer.dart @@ -11,6 +11,9 @@ final userReducers = combineReducers([ TypedReducer(_scrollToTop), TypedReducer(_toggleUpgrade), TypedReducer(_createNewWalletSuccess), + TypedReducer(_generateDIDSuccess), + TypedReducer( + _issueUserInfoCredentialSuccess), TypedReducer(_loginSuccess), TypedReducer(_loginVerifySuccess), TypedReducer(_logoutSuccess), @@ -76,6 +79,20 @@ UserState _getWalletDataSuccess(UserState state, GetWalletDataSuccess action) { ); } +UserState _generateDIDSuccess(UserState state, GenerateDIDSuccess action) { + return state.copyWith( + did: action.did, + privateKeyForDID: action.privateKeyForDID, + ); +} + +UserState _issueUserInfoCredentialSuccess( + UserState state, + IssueUserInfoCredentialSuccess action, +) { + return state.copyWith(userInfoCredential: action.userInfoCredential); +} + UserState _backupSuccess(UserState state, BackupSuccess action) { return state.copyWith(backup: true); } diff --git a/lib/redux/viewsmodels/account.dart b/lib/redux/viewsmodels/account.dart index b06f8b0a..3902bd9e 100644 --- a/lib/redux/viewsmodels/account.dart +++ b/lib/redux/viewsmodels/account.dart @@ -10,6 +10,7 @@ class AccountViewModel extends Equatable { final Function getSwapListBalances; final bool isBackup; final bool hasPreviousSessions; + final String? privateKeyForDID; const AccountViewModel({ required this.walletAddress, @@ -18,6 +19,7 @@ class AccountViewModel extends Equatable { required this.displayName, required this.isBackup, required this.hasPreviousSessions, + required this.privateKeyForDID, }); static AccountViewModel fromStore(Store store) { @@ -28,6 +30,7 @@ class AccountViewModel extends Equatable { displayName: store.state.userState.displayName, avatarUrl: store.state.userState.avatarUrl, walletAddress: store.state.userState.walletAddress, + privateKeyForDID: store.state.userState.privateKeyForDID, getSwapListBalances: () { store.dispatch(fetchSwapBalances()); }, diff --git a/lib/utils/did/did_service.dart b/lib/utils/did/did_service.dart new file mode 100644 index 00000000..1cdbd328 --- /dev/null +++ b/lib/utils/did/did_service.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; + +import 'package:didkit/didkit.dart'; +import 'package:fusecash/constants/strings.dart'; +import 'package:fusecash/models/verifiable_credential/issue_credential_options.dart'; +import 'package:fusecash/models/verifiable_credential/user_info_credential.dart'; +import 'package:fusecash/models/verifiable_credential/user_info_credential_subject.dart'; +import 'package:fusecash/models/verifiable_credential/verify_credential_options.dart'; +import 'package:intl/intl.dart'; +import 'package:uuid/uuid.dart'; + +class DIDService { + final String privateKey; + + const DIDService({required this.privateKey}); + + String generateDID() { + return DIDKit.keyToDID(Strings.defaultDIDMethod, privateKey); + } + + String issueUserInfoCredential({ + required String did, + required String name, + required String phoneNumber, + }) { + final userInfoCredentialModel = + _createUserInfoCredentialModel(did, name, phoneNumber); + final userInfoCredentialAsJson = jsonEncode(userInfoCredentialModel); + + final options = _createIssueCredentialOptions(); + final optionsAsJson = jsonEncode(options); + + return DIDKit.issueCredential( + userInfoCredentialAsJson, + optionsAsJson, + privateKey, + ); + } + + String verifyCredential(String credential) { + final optionsAsJson = _createOptionsAsJson(); + return DIDKit.verifyCredential(credential, optionsAsJson); + } + + String _createOptionsAsJson() { + const options = VerifyCredentialOptions(proofPurpose: "assertionMethod"); + return jsonEncode(options); + } + + IssueCredentialOptions _createIssueCredentialOptions() { + final verificationMethod = _getVerificationMethod(); + return IssueCredentialOptions( + proofPurpose: "assertionMethod", + verificationMethod: verificationMethod, + ); + } + + String _getVerificationMethod() { + return DIDKit.keyToVerificationMethod(Strings.defaultDIDMethod, privateKey); + } + + UserInfoCredential _createUserInfoCredentialModel( + String did, + String name, + String phoneNumber, + ) { + final id = _createCredentialID(); + final issuanceDate = _createIssuanceDate(); + + final userInfoCredentialSubject = UserInfoCredentialSubject( + id: did, + name: name, + phoneNumber: phoneNumber, + ); + + return UserInfoCredential( + id: id, + issuer: did, + issuanceDate: issuanceDate, + userInfoCredentialSubject: userInfoCredentialSubject, + ); + } + + String _createCredentialID() { + final uuid = const Uuid().v4(); + return "urn:uuid:$uuid"; + } + + String _createIssuanceDate() { + final dateFormatter = DateFormat("yyyy-MM-ddTHH:mm:ss"); + final now = DateTime.now(); + return "${dateFormatter.format(now)}Z"; + } +} diff --git a/lib/utils/did/private_key_generation.dart b/lib/utils/did/private_key_generation.dart new file mode 100644 index 00000000..08a857be --- /dev/null +++ b/lib/utils/did/private_key_generation.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip32/bip32.dart' as bip32; +import 'package:hex/hex.dart'; +import 'package:secp256k1/secp256k1.dart'; + +class PrivateKeyGeneration { + Future generatePrivateKey(String mnemonic) async { + //mnemonic - notice photo opera keen climb agent soft parrot best joke field devote + final seed = bip39.mnemonicToSeed( + mnemonic); //[105, 104, 114, 235, 191, 74, 81, 25, 186, 14, 224, 98, 187, 127, 45, 150, 115, 57, 174, 200, 238, 175, 36, 200, 142, 171, 91, 50, 40, 188, 126, 59, 73, 165, 227, 3, 92, 110, 15, 220, 157, 233, 140, 87, 195, 12, 91, 90, 165, 113, 52, 220, 139, 101, 206, 246, 2, 182, 24, 189, 73, 225, 195, 72] + + var rootKey = bip32.BIP32.fromSeed(seed); //Instance of 'BIP32' + + // derive path for ethereum '60' see bip 44, first address + final child = rootKey.derivePath("m/44'/60'/0'/0/0"); //Instance of 'BIP32' + + Iterable iterable = child + .privateKey!; //[44, 254, 73, 198, 41, 37, 89, 193, 190, 104, 116, 244, 188, 50, 31, 128, 25, 101, 57, 132, 49, 132, 105, 153, 166, 32, 39, 237, 145, 88, 63, 154] + + final epk = HEX.encode(List.from( + iterable)); //2cfe49c6292559c1be6874f4bc321f801965398431846999a62027ed91583f9a + + final pk = PrivateKey.fromHex(epk); //Instance of 'PrivateKey' + + final pub = pk.publicKey.toHex().substring( + 2); //41a72ac211e17bfa49bde064d4617ed88f08d394dfdbf20f51c367035d0dc9e8901f5ac8c57c64f9306b0ed200e08a1b36a08969dc1755f149eac0e3b652a06c + + final ad = HEX.decode( + epk); //[44, 254, 73, 198, 41, 37, 89, 193, 190, 104, 116, 244, 188, 50, 31, 128, 25, 101, 57, 132, 49, 132, 105, 153, 166, 32, 39, 237, 145, 88, 63, 154] + + final d = base64Url.encode(ad).substring(0, + 43); // remove "=" padding 43/44 // ad -> LP5JxiklWcG-aHT0vDIfgBllOYQxhGmZpiAn7ZFYP5o + + final mx = pub.substring(0, + 64); // first 32 bytes // mx -> 41a72ac211e17bfa49bde064d4617ed88f08d394dfdbf20f51c367035d0dc9e8 + + final ax = HEX.decode( + mx); //[65, 167, 42, 194, 17, 225, 123, 250, 73, 189, 224, 100, 212, 97, 126, 216, 143, 8, 211, 148, 223, 219, 242, 15, 81, 195, 103, 3, 93, 13, 201, 232] + + final x = base64Url.encode(ax).substring(0, + 43); // remove "=" padding 43/44 // x -> QacqwhHhe_pJveBk1GF-2I8I05Tf2_IPUcNnA10Nyeg + + final my = pub.substring( + 64); // last 32 bytes // my -> 901f5ac8c57c64f9306b0ed200e08a1b36a08969dc1755f149eac0e3b652a06c + + final ay = HEX.decode( + my); //[144, 31, 90, 200, 197, 124, 100, 249, 48, 107, 14, 210, 0, 224, 138, 27, 54, 160, 137, 105, 220, 23, 85, 241, 73, 234, 192, 227, 182, 82, 160, 108] + + final y = base64Url + .encode(ay) + .substring(0, 43); //kB9ayMV8ZPkwaw7SAOCKGzagiWncF1XxSerA47ZSoGw + + // ATTENTION !!!!! + // alg "ES256K-R" for did:ethr and did:tz2 "EcdsaSecp256k1RecoverySignature2020" + // use alg "ES256K" for did:key + final key = { + 'kty': 'EC', + 'crv': 'secp256k1', + 'd': d, + 'x': x, + 'y': y, + 'alg': 'ES256K' // or 'alg': "ES256K" for did:key + }; + // key = { + // "kty": "EC", + // "crv": "secp256k1", + // "d": "LP5JxiklWcG-aHT0vDIfgBllOYQxhGmZpiAn7ZFYP5o", + // "x": "QacqwhHhe_pJveBk1GF-2I8I05Tf2_IPUcNnA10Nyeg", + // "y": "kB9ayMV8ZPkwaw7SAOCKGzagiWncF1XxSerA47ZSoGw", + // "alg": "ES256K-R" + // } + + return jsonEncode(key); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 0d5c7af8..c1afd921 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -142,7 +142,7 @@ packages: source: hosted version: "2.0.0" bip39: - dependency: transitive + dependency: "direct main" description: name: bip39 url: "https://pub.dartlang.org" @@ -484,6 +484,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + didkit: + dependency: "direct main" + description: + path: "../didkit/lib/flutter" + relative: true + source: path + version: "0.0.2" dio: dependency: "direct main" description: @@ -797,6 +804,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.10.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" flutter_secure_storage: dependency: "direct main" description: @@ -1654,6 +1668,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1+1" + riverpod: + dependency: transitive + description: + name: riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" rxdart: dependency: transitive description: @@ -1668,6 +1689,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.0" + secp256k1: + dependency: "direct main" + description: + name: secp256k1 + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" sha3: dependency: transitive description: @@ -1806,6 +1834,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.10.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.2+1" stream_channel: dependency: transitive description: @@ -1947,7 +1982,7 @@ packages: source: hosted version: "3.0.1" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 352d0b95..40f0b9cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,13 @@ dependencies: flutter_localizations: sdk: flutter darq: ^1.2.1 + didkit: + path: ../didkit/lib/flutter + bip39: ^1.0.6 + secp256k1: ^0.3.0 + uuid: ^3.0.6 + flutter_riverpod: ^1.0.4 + dev_dependencies: flutter_native_splash: ^2.2.7 freezed: ^2.1.0+1 diff --git a/test/did_test/get_didkit_version_test.dart b/test/did_test/get_didkit_version_test.dart new file mode 100644 index 00000000..ac178071 --- /dev/null +++ b/test/did_test/get_didkit_version_test.dart @@ -0,0 +1,9 @@ +import 'package:didkit/didkit.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test("Should get DIDKit version", () { + final didKitVersion = DIDKit.getVersion(); + expect(didKitVersion, isA()); + }); +} \ No newline at end of file diff --git a/test/did_test/private_key_generation_test.dart b/test/did_test/private_key_generation_test.dart new file mode 100644 index 00000000..5a34778d --- /dev/null +++ b/test/did_test/private_key_generation_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:fusecash/utils/did/private_key_generation.dart'; + +void main() { + test( + "Should generate the same private key from the same mnemonic", + () async { + const mnemonic = "husband modify silk must mansion payment jeans reopen " + "connect eagle spirit wink"; + final privateKeyGeneration = PrivateKeyGeneration(); + final generatedPrivateKey = + await privateKeyGeneration.generatePrivateKey(mnemonic); + const privateKey = + '{"kty":"EC","crv":"secp256k1","d":"wTo43nJjOmx9aSeyZc3wAA7y4EHHEzvKcXB2A6t15iA","x":"1lUjK7OPOVgQjCKRAg_rvJTil0xHldjStkkH5OJp13k","y":"OQJFxHdalE1Zr2i02gZBopKUvNeO2LDjitkBzE4U2QI","alg":"ES256K"}'; + expect(generatedPrivateKey, equals(privateKey)); + }, + ); +}