Skip to content

Commit

Permalink
feat(cat-voices): wallet link service (#984)
Browse files Browse the repository at this point in the history
* refactor(cat-voices): migrate apple-mobile-web-app-capable to mobile-web-app-capable

* chore: add missing catalyst compression config

* chore: add word to dictionary

* feat: add loader to submit transaction button

* feat: add transaction config repository

* feat: add registration transaction builder

* feat: submit registration

* feat: update cryptocurrency formatter

* feat: handle submit tx error

* feat: move to next step after submitting a registration

* feat: improve error handling for submitting a transaction

* chore: typo

* chore: review feedback

* feat: localized exception

* refactor: RegistrationService

* feat: registration exception

* chore: resolve merge conflicts

* docs: document registration service

* fix: add missing exception

* fix: remove wrong export

* fix: convert dependencies to lazy singletons to fix dependency resolution

* style: reformat

* refactor: inject catalyst cardano

* refactor: convert to sealed class

* refactor: error handling
  • Loading branch information
dtscalac authored Oct 10, 2024
1 parent 73807b4 commit 008c637
Show file tree
Hide file tree
Showing 18 changed files with 276 additions and 76 deletions.
32 changes: 21 additions & 11 deletions catalyst_voices/lib/dependency/dependencies.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:catalyst_cardano/catalyst_cardano.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
import 'package:catalyst_voices_services/catalyst_voices_services.dart';
Expand Down Expand Up @@ -32,28 +33,37 @@ final class Dependencies extends DependencyProvider {
..registerFactory<RegistrationCubit>(() {
return RegistrationCubit(
downloader: get(),
transactionConfigRepository: get(),
registrationService: get(),
);
});
}

void _registerRepositories() {
this
..registerSingleton<CredentialsStorageRepository>(
CredentialsStorageRepository(storage: get()),
..registerLazySingleton<CredentialsStorageRepository>(
() => CredentialsStorageRepository(storage: get()),
)
..registerSingleton<AuthenticationRepository>(
AuthenticationRepository(credentialsStorageRepository: get()),
..registerLazySingleton<AuthenticationRepository>(
() => AuthenticationRepository(credentialsStorageRepository: get()),
)
..registerSingleton<TransactionConfigRepository>(
TransactionConfigRepository(),
..registerLazySingleton<TransactionConfigRepository>(
TransactionConfigRepository.new,
);
}

void _registerServices() {
registerSingleton<Storage>(const SecureStorage());
registerSingleton<Vault>(const SecureStorageVault());
registerSingleton<DummyAuthStorage>(const SecureDummyAuthStorage());
registerSingleton<Downloader>(Downloader());
registerLazySingleton<Storage>(() => const SecureStorage());
registerLazySingleton<Vault>(() => const SecureStorageVault());
registerLazySingleton<DummyAuthStorage>(
() => const SecureDummyAuthStorage(),
);
registerLazySingleton<Downloader>(Downloader.new);
registerLazySingleton<CatalystCardano>(() => CatalystCardano.instance);
registerLazySingleton<RegistrationService>(
() => RegistrationService(
get<TransactionConfigRepository>(),
get<CatalystCardano>(),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:catalyst_cardano/catalyst_cardano.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_services/catalyst_voices_services.dart';
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:result_type/result_type.dart';
Expand All @@ -19,15 +20,18 @@ abstract interface class WalletLinkManager {

final class WalletLinkCubit extends Cubit<WalletLinkStateData>
implements WalletLinkManager {
WalletLinkCubit() : super(const WalletLinkStateData());
final RegistrationService registrationService;

WalletLinkCubit({required this.registrationService})
: super(const WalletLinkStateData());

@override
Future<void> refreshWallets() async {
try {
emit(state.copyWith(wallets: const Optional.empty()));

final wallets =
await CatalystCardano.instance.getWallets().withMinimumDelay();
await registrationService.getCardanoWallets().withMinimumDelay();

emit(state.copyWith(wallets: Optional(Success(wallets))));
} on Exception catch (error, stackTrace) {
Expand All @@ -39,15 +43,8 @@ final class WalletLinkCubit extends Cubit<WalletLinkStateData>
@override
Future<bool> selectWallet(CardanoWallet wallet) async {
try {
final enabledWallet = await wallet.enable();
final balance = await enabledWallet.getBalance();
final address = await enabledWallet.getChangeAddress();

final walletDetails = CardanoWalletDetails(
wallet: wallet,
balance: balance.coin,
address: address,
);
final walletDetails =
await registrationService.getCardanoWalletDetails(wallet);

emit(state.copyWith(selectedWallet: Optional(walletDetails)));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:catalyst_voices_blocs/src/registration/cubits/recover_cubit.dart
import 'package:catalyst_voices_blocs/src/registration/cubits/wallet_link_cubit.dart';
import 'package:catalyst_voices_blocs/src/registration/state_data/keychain_state_data.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart';
import 'package:catalyst_voices_services/catalyst_voices_services.dart';
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart';
Expand All @@ -22,15 +21,17 @@ final class RegistrationCubit extends Cubit<RegistrationState> {
final KeychainCreationCubit _keychainCreationCubit;
final WalletLinkCubit _walletLinkCubit;
final RecoverCubit _recoverCubit;
final TransactionConfigRepository transactionConfigRepository;
final RegistrationService registrationService;

RegistrationCubit({
required Downloader downloader,
required this.transactionConfigRepository,
required this.registrationService,
}) : _keychainCreationCubit = KeychainCreationCubit(
downloader: downloader,
),
_walletLinkCubit = WalletLinkCubit(),
_walletLinkCubit = WalletLinkCubit(
registrationService: registrationService,
),
_recoverCubit = RecoverCubit(),
super(const RegistrationState()) {
_keychainCreationCubit.stream.listen(_onKeychainStateDataChanged);
Expand Down Expand Up @@ -118,35 +119,26 @@ final class RegistrationCubit extends Cubit<RegistrationState> {
),
);

// TODO(dtscalac): inject the networkId
const networkId = NetworkId.testnet;
final walletApi = await _walletLinkState.selectedCardanoWallet!.enable();

final registrationBuilder = RegistrationTransactionBuilder(
transactionConfig: await transactionConfigRepository.fetch(networkId),
networkId: networkId,
final unsignedTx = await registrationService.prepareRegistration(
wallet: _walletLinkState.selectedCardanoWallet!,
// TODO(dtscalac): inject the networkId
networkId: NetworkId.testnet,
seedPhrase: _keychainState.seedPhrase!,
roles: _walletLinkState.selectedRoles ?? _walletLinkState.defaultRoles,
changeAddress: await walletApi.getChangeAddress(),
rewardAddresses: await walletApi.getRewardAddresses(),
utxos: await walletApi.getUtxos(
amount: Balance(
coin: CardanoWalletDetails.minAdaForRegistration,
),
),
);

final tx = await registrationBuilder.build();
_onRegistrationStateDataChanged(
_registrationState.copyWith(
unsignedTx: Optional(Success(tx)),
unsignedTx: Optional(Success(unsignedTx)),
),
);
} on Exception catch (error, stackTrace) {
} on RegistrationException catch (error, stackTrace) {
_logger.severe('prepareRegistration', error, stackTrace);
_onRegistrationStateDataChanged(
_registrationState.copyWith(
unsignedTx: Optional(Failure(const LocalizedUnknownException())),
unsignedTx: Optional(
Failure(LocalizedRegistrationException.from(error)),
),
),
);
}
Expand All @@ -161,32 +153,24 @@ final class RegistrationCubit extends Cubit<RegistrationState> {
),
);

final walletApi = await _walletLinkState.selectedCardanoWallet!.enable();
final unsignedTx = _registrationState.unsignedTx!.success;
final witnessSet = await walletApi.signTx(transaction: unsignedTx);

final signedTx = Transaction(
body: unsignedTx.body,
isValid: true,
witnessSet: witnessSet,
auxiliaryData: unsignedTx.auxiliaryData,
final signedTx = await registrationService.submitRegistration(
wallet: _walletLinkState.selectedCardanoWallet!,
unsignedTx: _registrationState.unsignedTx!.success,
);

await walletApi.submitTx(transaction: signedTx);

_onRegistrationStateDataChanged(
_registrationState.copyWith(
submittedTx: Optional(Success(signedTx)),
isSubmittingTx: false,
),
);
nextStep();
} on Exception catch (error, stackTrace) {
} on RegistrationException catch (error, stackTrace) {
_logger.severe('submitRegistration', error, stackTrace);
_onRegistrationStateDataChanged(
_registrationState.copyWith(
submittedTx: Optional(
Failure(const LocalizedRegistrationTransactionException()),
Failure(LocalizedRegistrationException.from(error)),
),
isSubmittingTx: false,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,12 @@ abstract class VoicesLocalizations {
/// **'Transaction failed'**
String get registrationTransactionFailed;

/// Indicates an error when preparing a transaction has failed due to low wallet balance.
///
/// In en, this message translates to:
/// **'Insufficient balance, please top up your wallet.'**
String get registrationInsufficientBalance;

/// A title on the role chooser screen in registration.
///
/// In en, this message translates to:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,9 @@ class VoicesLocalizationsEn extends VoicesLocalizations {
@override
String get registrationTransactionFailed => 'Transaction failed';

@override
String get registrationInsufficientBalance => 'Insufficient balance, please top up your wallet.';

@override
String get walletLinkRoleChooserTitle => 'How do you want to participate in Catalyst?';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,9 @@ class VoicesLocalizationsEs extends VoicesLocalizations {
@override
String get registrationTransactionFailed => 'Transaction failed';

@override
String get registrationInsufficientBalance => 'Insufficient balance, please top up your wallet.';

@override
String get walletLinkRoleChooserTitle => 'How do you want to participate in Catalyst?';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,10 @@
"@registrationTransactionFailed": {
"description": "Indicates an error when submitting a registration transaction failed."
},
"registrationInsufficientBalance": "Insufficient balance, please top up your wallet.",
"@registrationInsufficientBalance": {
"description": "Indicates an error when preparing a transaction has failed due to low wallet balance."
},
"walletLinkRoleChooserTitle": "How do you want to participate in Catalyst?",
"@walletLinkRoleChooserTitle": {
"description": "A title on the role chooser screen in registration."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:equatable/equatable.dart';

/// A base exception thrown during user registration.
sealed class RegistrationException with EquatableMixin implements Exception {
const RegistrationException();

@override
List<Object?> get props => [];
}

/// An exception thrown when attempting to register
/// but the user doesn't have enough Ada to cover the transaction fee.
final class RegistrationInsufficientBalanceException
extends RegistrationException {
const RegistrationInsufficientBalanceException();

@override
String toString() => 'RegistrationInsufficientBalanceException';
}

/// An exception thrown when attempting to register and the transaction fails.
final class RegistrationTransactionException extends RegistrationException {
const RegistrationTransactionException();

@override
String toString() => 'RegistrationTransactionException';
}

/// An exception thrown when attempting to register and the transaction fails.
final class RegistrationUnknownException extends RegistrationException {
const RegistrationUnknownException();

@override
String toString() => 'RegistrationUnknownException';
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export 'create_account_type.dart';
export 'create_keychain_stage.dart';
export 'exception/registration_exception.dart';
export 'recover_seed_phrase_stage.dart';
export 'registration_recover_method.dart';
export 'registration_step.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';

/// Manages the [TransactionBuilderConfig].
class TransactionConfigRepository {
final class TransactionConfigRepository {
/// Returns the current [TransactionBuilderConfig] for given [network].
///
/// In the future this might communicate with a blockchain
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export 'downloader/downloader.dart';
export 'registration/registration_service.dart';
export 'registration/registration_transaction_builder.dart';
export 'storage/dummy_auth_storage.dart';
export 'storage/secure_storage.dart';
export 'storage/storage.dart';
export 'storage/vault/lock_factor.dart';
export 'storage/vault/lock_factor_codec.dart' show LockFactorCodec;
export 'storage/vault/secure_storage_vault.dart';
export 'storage/vault/vault.dart';
export 'transaction/registration_transaction_builder.dart';
Loading

0 comments on commit 008c637

Please sign in to comment.