Skip to content

Commit

Permalink
BREAKING CHANGE: rework subscription logic, now TonWalletState & Toke…
Browse files Browse the repository at this point in the history
…nWalletState available (#92)
  • Loading branch information
Alex-A4 authored Nov 2, 2023
1 parent 7f43dca commit 63db2fc
Show file tree
Hide file tree
Showing 10 changed files with 945 additions and 443 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'token_wallet_ordinary_transaction.dart';
export 'token_wallet_state.dart';
export 'token_wallet_subscription.dart';
62 changes: 62 additions & 0 deletions lib/src/models/token_wallet_related/token_wallet_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:nekoton_repository/nekoton_repository.dart';

/// State of [TokenWallet] that allows tracking when subscription was created
/// successfully or when it failed with some error.
///
/// To detect which state is active, use [hasWallet] and [hasError].
@immutable
class TokenWalletState extends Equatable {
/// Create state with error.
const TokenWalletState.error({
required Object err,
required this.owner,
required this.rootTokenContract,
}) : wallet = null,
error = err;

/// Create state with wallet
TokenWalletState.wallet(TokenWallet w)
: error = null,
owner = w.owner,
rootTokenContract = w.rootTokenContract,
wallet = w;

/// Allows to track for which wallet this state was created.
/// This can be useful when [wallet] is null and we cannot detect it.
final Address owner;
final Address rootTokenContract;

/// Wallet that could be created.
/// If wallet was created, then [hasWallet] returns true and you can use it
/// as usual.
final TokenWallet? wallet;

/// Any error that could be thrown during creating subscription.
/// Typically, this is [FfiException] or
/// [TokenWalletRetrySubscriptionMissedAsset], but may be any other type.
final Object? error;

bool get hasWallet => wallet != null;

bool get hasError => error != null;

@override
List<Object?> get props => [owner, rootTokenContract, error, wallet];
}

/// Exception that will be thrown from any methods when user outside package
/// called method without making sure that state was initialized.
class TokenWalletStateNotInitializedException implements Exception {
@override
String toString() => '''
`TokenWalletState.wallet` was not initialized.
Try calling `TokenWalletRepository.retrySubscriptions`
''';
}

/// Exception that will be thrown from
/// [TokenWalletRepository.retryTokenSubscription] when asset with specified
/// owner and rootTokenContract won't be found.
class TokenWalletRetrySubscriptionMissedAsset implements Exception {}
1 change: 1 addition & 0 deletions lib/src/models/ton_wallet_related/ton_wallet_related.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export 'ton_wallet_multisig_ordinary_transaction.dart';
export 'ton_wallet_multisig_pending_transaction.dart';
export 'ton_wallet_ordinary_transaction.dart';
export 'ton_wallet_pending_transaction.dart';
export 'ton_wallet_state.dart';
export 'ton_wallet_subscription.dart';
58 changes: 58 additions & 0 deletions lib/src/models/ton_wallet_related/ton_wallet_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:nekoton_repository/nekoton_repository.dart';

/// State of [TonWallet] that allows tracking when subscription was created
/// successfully or when it failed with some error.
///
/// To detect which state is active, use [hasWallet] and [hasError].
@immutable
class TonWalletState extends Equatable {
/// Create state with error.
const TonWalletState.error({
required Object err,
required this.address,
}) : wallet = null,
error = err;

/// Create state with wallet
TonWalletState.wallet(TonWallet w)
: error = null,
address = w.address,
wallet = w;

/// Allows to track for which wallet this state was created.
/// This can be useful when [wallet] is null and we cannot detect it.
final Address address;

/// Wallet that could be created.
/// If wallet was created, then [hasWallet] returns true and you can use it
/// as usual.
final TonWallet? wallet;

/// Any error that could be thrown during creating subscription.
/// Typically, this is [FfiException] or
/// [TonWalletRetrySubscriptionMissedAsset] but may be any other type.
final Object? error;

bool get hasWallet => wallet != null;

bool get hasError => error != null;

@override
List<Object?> get props => [address, error, wallet];
}

/// Exception that will be thrown from any methods when user outside package
/// called method without making sure that state was initialized.
class TonWalletStateNotInitializedException implements Exception {
@override
String toString() => '''
`TonWalletState.wallet` was not initialized.
Try calling `TonWalletRepository.retrySubscriptions`
''';
}

/// Exception that will be thrown from [TonWalletRepository.retrySubscriptions]
/// when asset with specified address won't be found
class TonWalletRetrySubscriptionMissedAsset implements Exception {}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@ abstract class TokenWalletRepository {
///
/// !!! You do not need to refresh wallet directly after subscribing, because
/// it will load its state during creation.
Future<TokenWallet> subscribeToken({
Future<TokenWalletState> subscribeToken({
required Address owner,
required Address rootTokenContract,
});

/// If creating of subscription was failed and [TokenWalletState] contains
/// error, then you can call this method to try to update this subscription.
///
/// If subscription creates successfully, then it will update cache & stream.
///
/// If the asset with [owner] and [rootTokenContract] won't be found, then
/// method provides error state to stream.
Future<void> retryTokenSubscription(Address owner, Address rootTokenContract);

/// Start polling for wallet state updates by its address.
///
/// Only one wallet should be polled at the same time, this is necessary to
Expand All @@ -27,6 +36,9 @@ abstract class TokenWalletRepository {
/// [TokenWallet.refresh].
/// directly, but for real polling, you must use this method.
///
/// If [TokenWalletState.wallet] was null (wallet was not created), polling
/// will be ignored.
///
/// [refreshInterval] - time to poll requests, default
/// [tonWalletRefreshInterval].
/// [stopPrevious] - if previously created pollers should be stopped,
Expand Down Expand Up @@ -103,7 +115,7 @@ abstract class TokenWalletRepository {
/// Get instance of wallet that was added by [subscribeToken].
/// This method will throw error if there is no wallet that had been added
/// before.
TokenWallet getTokenWallet(Address owner, Address rootTokenContract);
TokenWalletState getTokenWallet(Address owner, Address rootTokenContract);

/// Map list of transactions for TokenWallet to list of
/// [TokenWalletOrdinaryTransaction].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
/// Subject that allows listening for wallets subscribing/unsubscribing
/// Key - pair where first item is owner address, second is rootTokenContract
final _tokenWalletsSubject =
BehaviorSubject<Map<(Address, Address), TokenWallet>>.seeded({});
BehaviorSubject<Map<(Address, Address), TokenWalletState>>.seeded({});

/// How many tokens can be subscribed at time for one cycle in
/// [TokenWalletRepositoryImpl._updateTokenSubscriptionsPairs].
Expand Down Expand Up @@ -60,15 +60,15 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
<(Address, Address), TokenWalletSubscription>{};

/// Listen for wallets subscribing/unsubscribing
Stream<List<TokenWallet>> get tokenWalletsStream =>
Stream<List<TokenWalletState>> get tokenWalletsStream =>
_tokenWalletsSubject.stream.map((e) => e.values.toList());

/// Get current available subscriptions for wallets
List<TokenWallet> get tokenWallets =>
List<TokenWalletState> get tokenWallets =>
_tokenWalletsSubject.value.values.toList();

/// Get current available subscriptions for wallets as map
Map<(Address, Address), TokenWallet> get tokenWalletsMap =>
Map<(Address, Address), TokenWalletState> get tokenWalletsMap =>
_tokenWalletsSubject.value;

/// Queues for polling active wallets.
Expand All @@ -80,7 +80,7 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
final tokenPollingQueues = <(Address, Address), RefreshPollingQueue>{};

@override
Future<TokenWallet> subscribeToken({
Future<TokenWalletState> subscribeToken({
required Address owner,
required Address rootTokenContract,
}) async {
Expand All @@ -95,6 +95,34 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
return addTokenWalletInst(wallet);
}

@override
Future<void> retryTokenSubscription(
Address owner,
Address rootTokenContract,
) async {
final asset = lastUpdatedTokenAssets
?.map(
(e) => e.additionalAssets[lastUpdatedNetworkGroup]?.tokenWallets
.map((el) => (e.tonWallet.address, el.rootTokenContract)),
)
.whereNotNull()
.expand((e) => e)
.firstWhereOrNull((e) => e.$1 == owner && e.$2 == rootTokenContract);

if (asset == null) {
tokenWalletsMap[(owner, rootTokenContract)] = TokenWalletState.error(
err: TokenWalletRetrySubscriptionMissedAsset(),
owner: owner,
rootTokenContract: rootTokenContract,
);
_tokenWalletsSubject.add(tokenWalletsMap);

return;
}

return _subscribeTokenAsset(asset);
}

@override
void startPollingToken(
Address owner,
Expand All @@ -115,7 +143,9 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
stopPollingToken();
}

final wallet = getTokenWallet(owner, rootTokenContract);
final wallet = getTokenWallet(owner, rootTokenContract).wallet;
if (wallet == null) return;

tokenPollingQueues[pair] = RefreshPollingQueue(
refreshInterval: refreshInterval,
refresher: wallet,
Expand All @@ -134,15 +164,15 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
void unsubscribeToken(Address owner, Address rootTokenContract) {
final wallet = removeTokenWalletInst(owner, rootTokenContract);
tokenPollingQueues.remove((owner, rootTokenContract))?.stopPolling();
wallet?.dispose();
wallet?.wallet?.dispose();
}

@override
void closeAllTokenSubscriptions() {
stopPollingToken();

for (final wallet in tokenWallets) {
wallet.dispose();
wallet.wallet?.dispose();
}

_tokenWalletsSubject.add({});
Expand Down Expand Up @@ -180,7 +210,7 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
List<(Address, Address)> newWallets,
) async {
final toSubscribe = <(Address, Address)>[];
final toUnsubscribe = <TokenWallet>[];
final toUnsubscribe = <TokenWalletState>[];

// Stop last created operation if possible
final oldOperation = _lastOperation;
Expand Down Expand Up @@ -215,18 +245,7 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
final parts = partition(toSubscribe, tokenSubscribeAtTimeAmount);

for (final part in parts) {
await Future.wait(
part.map((wallet) async {
try {
await subscribeToken(
owner: wallet.$1,
rootTokenContract: wallet.$2,
);
} catch (e, t) {
_logger.severe('_updateTokenSubscriptionsPairs', e, t);
}
}),
);
await Future.wait(part.map(_subscribeTokenAsset));

// Make this pseudo event to allow other operations in event loop
// to be executed
Expand All @@ -242,6 +261,26 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
await operation.valueOrCancellation();
}

Future<void> _subscribeTokenAsset((Address, Address) wallet) async {
try {
await subscribeToken(
owner: wallet.$1,
rootTokenContract: wallet.$2,
);
} catch (e, t) {
_logger.severe('_subscribeTokenAsset', e, t);

// Save error state of wallet
final res = TokenWalletState.error(
err: e,
owner: wallet.$1,
rootTokenContract: wallet.$2,
);
tokenWalletsMap[wallet] = res;
_tokenWalletsSubject.add(tokenWalletsMap);
}
}

@override
Future<void> updateTokenTransportSubscriptions() async {
// Stop last created operation if possible
Expand Down Expand Up @@ -283,7 +322,9 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
BigInt? attachedAmount,
String? payload,
}) async {
final tokenWallet = getTokenWallet(owner, rootTokenContract);
final tokenWallet = getTokenWallet(owner, rootTokenContract).wallet;

if (tokenWallet == null) throw TokenWalletStateNotInitializedException();

return tokenWallet.prepareTransfer(
destination: destination,
Expand All @@ -300,17 +341,19 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
required Address rootTokenContract,
required String fromLt,
}) {
final tokenWallet = getTokenWallet(owner, rootTokenContract);
final tokenWallet = getTokenWallet(owner, rootTokenContract).wallet;

if (tokenWallet == null) throw TokenWalletStateNotInitializedException();

return tokenWallet.preloadTransactions(fromLt: fromLt);
}

@override
TokenWallet getTokenWallet(Address owner, Address rootTokenContract) {
TokenWalletState getTokenWallet(Address owner, Address rootTokenContract) {
final wallet = tokenWalletsMap[(owner, rootTokenContract)];
if (wallet == null) {
throw Exception(
'TokenWallet ($owner, $rootTokenContract) not found',
'TokenWalletState ($owner, $rootTokenContract) not found',
);
}

Expand All @@ -321,21 +364,25 @@ mixin TokenWalletRepositoryImpl implements TokenWalletRepository {
/// You must not call this method directly form app, use [subscribeToken].
@protected
@visibleForTesting
TokenWallet addTokenWalletInst(TokenWallet wallet) {
TokenWalletState addTokenWalletInst(TokenWallet wallet) {
final wallets = tokenWalletsMap;
final pair = (wallet.owner, wallet.rootTokenContract);
wallets[pair] = wallet;
final res = TokenWalletState.wallet(wallet);
wallets[pair] = res;
tokenWalletSubscriptions[pair] = _createWalletSubscription(wallet);
_tokenWalletsSubject.add(wallets);

return wallet;
return res;
}

/// This is internal method to remove wallet from cache.
/// You must not call this method directly form app, use [unsubscribeToken].
@protected
@visibleForTesting
TokenWallet? removeTokenWalletInst(Address owner, Address rootTokenContract) {
TokenWalletState? removeTokenWalletInst(
Address owner,
Address rootTokenContract,
) {
final wallets = tokenWalletsMap;
final pair = (owner, rootTokenContract);
final wallet = wallets.remove(pair);
Expand Down
Loading

0 comments on commit 63db2fc

Please sign in to comment.