diff --git a/.github/workflows/ios_integration_test.yml b/.github/workflows/ios_integration_test.yml index 5858993af..65f34017c 100644 --- a/.github/workflows/ios_integration_test.yml +++ b/.github/workflows/ios_integration_test.yml @@ -30,9 +30,10 @@ jobs: record_video: true # With this flag we can run the CI against different node versions to check compatibility. docker_tag: "1.5.4" - - device: "iPhone 8 Plus" - record_video: false - docker_tag: "1.5.4" + # IPhone 8 is broken currently. +# - device: "iPhone 8 Plus" +# record_video: false +# docker_tag: "1.5.4" - device: "iPad Pro (12.9-inch) (6th generation)" record_video: false docker_tag: "1.5.4" diff --git a/app/lib/main.dart b/app/lib/main.dart index 118eee1c0..1de776949 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -15,7 +15,6 @@ import 'package:encointer_wallet/utils/repository_provider.dart'; import 'package:encointer_wallet/modules/modules.dart'; import 'package:encointer_wallet/service/notification/lib/notification.dart'; import 'package:encointer_wallet/store/connectivity/connectivity_store.dart'; -import 'package:encointer_wallet/service/substrate_api/core/dart_api.dart'; import 'package:encointer_wallet/service/http_overrides.dart'; import 'package:encointer_wallet/store/app.dart'; import 'package:encointer_wallet/utils/local_storage.dart' as util; @@ -36,7 +35,6 @@ Future main({AppConfig? appConfig, AppSettings? settings}) async { providers: [ RepositoryProvider(create: (context) => EwHttp()), RepositoryProvider(create: (context) => appConfig ?? const AppConfig()), - RepositoryProvider(create: (context) => SubstrateDartApi()), ], child: MultiProvider( providers: [ diff --git a/app/lib/page/assets/index.dart b/app/lib/page/assets/index.dart index c3553cfd8..0377e7f90 100644 --- a/app/lib/page/assets/index.dart +++ b/app/lib/page/assets/index.dart @@ -400,7 +400,6 @@ class _AssetsViewState extends State { void _connectNodeAll() { // if network connected failed, reconnect if (!widget.store.settings.loading) { - widget.store.settings.setNetworkLoading(true); webApi.init(); } } diff --git a/app/lib/service/init_web_api/init_web_api.dart b/app/lib/service/init_web_api/init_web_api.dart index 985b72252..c89441097 100644 --- a/app/lib/service/init_web_api/init_web_api.dart +++ b/app/lib/service/init_web_api/init_web_api.dart @@ -1,7 +1,6 @@ import 'package:encointer_wallet/config.dart'; import 'package:encointer_wallet/service/log/log_service.dart'; import 'package:encointer_wallet/service/substrate_api/api.dart'; -import 'package:encointer_wallet/service/substrate_api/core/dart_api.dart'; import 'package:encointer_wallet/store/app.dart'; import 'package:encointer_wallet/utils/repository_provider.dart'; import 'package:ew_http/ew_http.dart'; @@ -14,8 +13,7 @@ import 'package:flutter/material.dart'; Future initWebApi(BuildContext context, AppStore store) async { final ewHttp = RepositoryProvider.of(context); final appConfig = RepositoryProvider.of(context); - final dartApi = RepositoryProvider.of(context); - webApi = Api.create(store, dartApi, ewHttp, isIntegrationTest: appConfig.isIntegrationTest); + webApi = Api.create(store, ewHttp, isIntegrationTest: appConfig.isIntegrationTest); await webApi.init().timeout( const Duration(seconds: 20), diff --git a/app/lib/service/substrate_api/api.dart b/app/lib/service/substrate_api/api.dart index e30b051bb..77a099412 100644 --- a/app/lib/service/substrate_api/api.dart +++ b/app/lib/service/substrate_api/api.dart @@ -19,10 +19,9 @@ import 'package:encointer_wallet/service/log/log_service.dart'; late Api webApi; class Api { - const Api( + Api( this.store, this.provider, - this.dartApi, this.account, this.assets, this.chain, @@ -32,7 +31,6 @@ class Api { factory Api.create( AppStore store, - SubstrateDartApi dartApi, EwHttp ewHttp, { bool isIntegrationTest = false, }) { @@ -40,11 +38,10 @@ class Api { return Api( store, provider, - dartApi, AccountApi(store, provider), AssetsApi(store, EncointerKusama(provider)), ChainApi(store, provider), - EncointerApi(store, dartApi, ewHttp, EncointerKusama(provider)), + EncointerApi(store, SubstrateDartApi(provider), ewHttp, EncointerKusama(provider)), isIntegrationTest ? MockIpfsApi(ewHttp) : IpfsApi(ewHttp, gateway: store.settings.ipfsGateway), ); } @@ -52,20 +49,57 @@ class Api { final AppStore store; final ReconnectingWsProvider provider; - final SubstrateDartApi dartApi; final AccountApi account; final AssetsApi assets; final ChainApi chain; final EncointerApi encointer; final IpfsApi ipfsApi; + Future? _connecting; + + /// Timer to regularly check for network connections. This periodic timer will be + /// paused when the app goes into background, and resumes when the app comes into + /// the foreground again. + Timer? _timer; + Future init() async { - await Future.wait([ - dartApi.connect(store.settings.endpoint.value!), - provider.connectToNewEndpoint(Uri.parse(store.settings.endpoint.value!)), - ]); + await close(); + _connecting = _connect(); + + _timer = Timer.periodic(const Duration(seconds: 10), (timer) async { + if (!provider.isConnected()) { + if (_connecting == null) { + Log.p('[webApi] provider is disconnected. Trying to connect again...'); + await close(); + _connecting = _connect(); + } else { + Log.p('[webApi] still trying to connect..'); + } + } + }); + } - Log.d('Connected to endpoint: ${store.settings.endpoint.value!}', 'Api'); + Future _connect() { + Log.d('[webApi] Connecting to endpoint: ${store.settings.endpoint.value!}', 'Api'); + + store.settings.setNetworkLoading(true); + + final endpoint = store.settings.endpoint.value!; + return provider.connectToNewEndpoint(Uri.parse(endpoint)).then((voidValue) async { + Log.p('[webApi] channel is ready...'); + if (await isConnected()) { + return _onConnected(); + } else { + Log.p('[webApi] connection failed will try again...'); + } + }).catchError((dynamic error) { + // mostly timeouts if the endpoint is not available + Log.e('[webApi] error during connection: $error}'); + }).whenComplete(() => _connecting == null); + } + + Future _onConnected() async { + Log.d('[webApi] Connected to endpoint: ${store.settings.endpoint.value!}', 'Api'); if (store.account.currentAddress.isNotEmpty) { await store.encointer.initializeUninitializedStores(store.account.currentAddress); @@ -84,7 +118,7 @@ class Api { store.settings.setNetworkLoading(false); - Log.d('Obtained basic network data: ${store.settings.endpoint.value!}', 'Api'); + Log.d('[webApi] Obtained basic network data: ${store.settings.endpoint.value!}'); // need to do this from here as we can't access instance fields in constructor. account.setFetchAccountData(fetchAccountData); @@ -93,13 +127,21 @@ class Api { } Future close() async { + _timer?.cancel(); + _timer = null; + _connecting = null; + final futures = [ - stopSubscriptions(), - encointer.close(), - provider.disconnect(), + stopSubscriptions() + .timeout(const Duration(seconds: 5), onTimeout: () => Log.e('[webApi] stopping subscriptions timeout')), + provider + .disconnect() + .timeout(const Duration(seconds: 5), onTimeout: () => Log.e('[webApi] provider disconnect timeout')), ]; await Future.wait(futures); + + Log.d('[webApi] Closed webApi connections'); } void fetchAccountData() { @@ -115,8 +157,8 @@ class Api { ]); } - Future stopSubscriptions() { - return Future.wait([ + Future stopSubscriptions() async { + await Future.wait([ encointer.stopSubscriptions(), chain.stopSubscriptions(), assets.stopSubscriptions(), @@ -124,12 +166,10 @@ class Api { } Future isConnected() async { - final dartConnected = dartApi.isConnected(); final providerConnected = provider.isConnected(); - Log.d('Dart Rpc Api is connected: $dartConnected', 'Api'); - Log.d('Provider is connected: $providerConnected', 'Api'); + Log.d('[webApi] Provider is connected: $providerConnected'); - return dartConnected && providerConnected; + return providerConnected; } } diff --git a/app/lib/service/substrate_api/core/dart_api.dart b/app/lib/service/substrate_api/core/dart_api.dart index 400bb4186..58fe498ad 100644 --- a/app/lib/service/substrate_api/core/dart_api.dart +++ b/app/lib/service/substrate_api/core/dart_api.dart @@ -1,15 +1,17 @@ import 'dart:async'; -import 'package:encointer_wallet/service/substrate_api/core/reconnecting_ws_provider.dart'; import 'package:encointer_wallet/models/index.dart'; import 'package:encointer_wallet/service/log/log_service.dart'; +import 'package:ew_polkadart/ew_polkadart.dart'; /// Api to talk to an substrate node via the websocket protocol. /// /// Once connected, a websocket channel is maintained until closed by either side. class SubstrateDartApi { + SubstrateDartApi(this._provider); + /// Websocket client used to connect to the node. - ReconnectingWsProvider? _provider; + final Provider _provider; /// The rpc methods exposed by the connected node. RpcMethods? _rpc; @@ -18,16 +20,14 @@ class SubstrateDartApi { String? _endpoint; /// Returns the rpc nodes of the connected node or an empty list otherwise. - List? get rpcMethods { - return _rpc != null ? _rpc!.methods : []; + Future rpcMethods() async { + return rpc>('rpc_methods', []).then(RpcMethods.fromJson); } /// Gets address of the node we connect to including ws(s). String? get endpoint => _endpoint; Future connect(String endpoint) async { - _connectAndListen(endpoint); - try { _rpc = await rpc>('rpc_methods', []).then(RpcMethods.fromJson); @@ -44,48 +44,16 @@ class SubstrateDartApi { } } - /// Closes the websocket connection. - Future close() async { - if (_provider != null) { - await _provider!.disconnect(); - } else { - Log.d('no connection to be closed.', 'SubstrateDartApi'); - } - } - /// Queries the rpc of the node. /// /// Hints: /// * account ids must be passed as SS58. Future rpc(String method, List params) async { - if (_provider == null) { - throw Exception("[dartApi] Can't call an rpc method because we are not connected to an endpoint"); - } - if (!_provider!.isConnected()) { - Log.d('[dartApi] not connected. trying to reconnect to $endpoint', 'SubstrateDartApi'); - reconnect(); - Log.d('[dartApi] connection status: isConnected? ${_provider?.isConnected()}', 'SubstrateDartApi'); - } - final response = await _provider!.send(method, params); + final response = await _provider.send(method, params); if (response.error != null) throw Exception(response.error); final data = response.result! as T; return data; } - - bool isConnected() { - return _provider!.isConnected(); - } - - /// Reconnect to the same endpoint if the connection was closed. - void reconnect() { - if (endpoint != null) _connectAndListen(endpoint!); - } - - /// Connects to and endpoint and starts listening on the input stream. - void _connectAndListen(String endpoint) { - _endpoint = endpoint; - _provider = ReconnectingWsProvider(Uri.parse(endpoint)); - } } diff --git a/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart b/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart index b1c694578..ab4aaa7c1 100644 --- a/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart +++ b/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart @@ -16,6 +16,7 @@ class ReconnectingWsProvider extends Provider { Future connectToNewEndpoint(Uri url) async { await disconnect(); provider = WsProvider(url); + await provider.ready(); } @override @@ -39,7 +40,9 @@ class ReconnectingWsProvider extends Provider { return Future.value(); } else { try { - await provider.disconnect(); + // Disconnect runs into a timeout if our endpoint doesn't exist for some reason. + await provider.disconnect().timeout(const Duration(seconds: 3), + onTimeout: () => Log.e('Timeout in disconnecting', 'ReconnectingWsProvider')); } catch (e) { Log.e('Error disconnecting websocket: $e', 'ReconnectingWsProvider'); return Future.value(); diff --git a/app/lib/service/substrate_api/encointer/encointer_api.dart b/app/lib/service/substrate_api/encointer/encointer_api.dart index 56b40180d..235d800d3 100644 --- a/app/lib/service/substrate_api/encointer/encointer_api.dart +++ b/app/lib/service/substrate_api/encointer/encointer_api.dart @@ -84,11 +84,6 @@ class EncointerApi { await _businessRegistry?.cancel(); } - Future close() async { - Log.d('[EncointerApi: closing', 'EncointerApi'); - return _dartApi.close(); - } - void getCommunityData() { getBusinesses(); getCommunityMetadata(); diff --git a/app/lib/service/substrate_api/encointer/encointer_dart_api.dart b/app/lib/service/substrate_api/encointer/encointer_dart_api.dart index 659dab003..41c9ec4dd 100644 --- a/app/lib/service/substrate_api/encointer/encointer_dart_api.dart +++ b/app/lib/service/substrate_api/encointer/encointer_dart_api.dart @@ -20,11 +20,6 @@ class EncointerDartApi { final SubstrateDartApi _dartApi; - Future close() async { - Log.d('[EncointerDartApi: closing', 'EncointerDartApi'); - return _dartApi.close(); - } - /// Queries the rpc 'encointer_getAggregatedAccountData'. /// Future getAggregatedAccountData(CommunityIdentifier cid, String account, {BlockHash? at}) { diff --git a/app/lib/store/settings.dart b/app/lib/store/settings.dart index 338dbbd02..9d03581f4 100644 --- a/app/lib/store/settings.dart +++ b/app/lib/store/settings.dart @@ -169,8 +169,6 @@ abstract class _SettingsStore with Store { } Future reloadNetwork(EndpointData network) async { - setNetworkLoading(true); - // Stop networking before loading cache await webApi.close(); diff --git a/app/test/mock/api/mock_api.dart b/app/test/mock/api/mock_api.dart index 6ab753850..d11fb350f 100644 --- a/app/test/mock/api/mock_api.dart +++ b/app/test/mock/api/mock_api.dart @@ -15,19 +15,18 @@ import 'mock_polkadart_provider.dart'; import 'mock_substrate_dart_api.dart'; MockApi getMockApi(AppStore store) { - return MockApi(store, MockSubstrateDartApi(), EwHttp()); + return MockApi(store, EwHttp()); } class MockApi extends Api { - MockApi(AppStore store, MockSubstrateDartApi dartApi, EwHttp ewHttp) + MockApi(AppStore store, EwHttp ewHttp) : super( store, MockPolkadartProvider(), - dartApi, MockAccountApi(store, MockPolkadartProvider()), MockAssetsApi(store, MockEncointerKusamaApi()), MockChainApi(store, MockPolkadartProvider()), - MockEncointerApi(store, dartApi, ewHttp, MockEncointerKusamaApi()), + MockEncointerApi(store, MockSubstrateDartApi(MockPolkadartProvider()), ewHttp, MockEncointerKusamaApi()), MockIpfsApi(ewHttp), ); diff --git a/app/test/mock/api/mock_substrate_dart_api.dart b/app/test/mock/api/mock_substrate_dart_api.dart index 032e311e0..7e442b4ae 100644 --- a/app/test/mock/api/mock_substrate_dart_api.dart +++ b/app/test/mock/api/mock_substrate_dart_api.dart @@ -1,3 +1,5 @@ import 'package:encointer_wallet/service/substrate_api/core/dart_api.dart'; -class MockSubstrateDartApi extends SubstrateDartApi {} +class MockSubstrateDartApi extends SubstrateDartApi { + MockSubstrateDartApi(super.provider); +} diff --git a/app/test/service/substrate_api/core/dart_api_test.dart b/app/test/service/substrate_api/core/dart_api_test.dart index 8b7111b4d..793386003 100644 --- a/app/test/service/substrate_api/core/dart_api_test.dart +++ b/app/test/service/substrate_api/core/dart_api_test.dart @@ -1,3 +1,4 @@ +import 'package:encointer_wallet/service/substrate_api/core/reconnecting_ws_provider.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:encointer_wallet/service/substrate_api/core/dart_api.dart'; @@ -7,13 +8,13 @@ import '../../../utils/test_tags.dart'; void main() { group('can connect', () { test('rpc methods contains getAggregatedAccountData', () async { - final encointerApi = SubstrateDartApi(); + final provider = ReconnectingWsProvider(Uri.parse('ws://localhost:9944')); - await encointerApi.connect('ws://localhost:9944'); + final encointerApi = SubstrateDartApi(provider); - expect(encointerApi.rpcMethods?.contains('encointer_getAggregatedAccountData'), true); + expect((await encointerApi.rpcMethods()).methods!.contains('encointer_getAggregatedAccountData'), true); - await encointerApi.close(); + await provider.disconnect(); }, tags: encointerNodeE2E); }); } diff --git a/app/test/service/substrate_api/encointer/encointer_dart_api_test.dart b/app/test/service/substrate_api/encointer/encointer_dart_api_test.dart index ae76d60b4..00c32b555 100644 --- a/app/test/service/substrate_api/encointer/encointer_dart_api_test.dart +++ b/app/test/service/substrate_api/encointer/encointer_dart_api_test.dart @@ -1,3 +1,4 @@ +import 'package:encointer_wallet/service/substrate_api/core/reconnecting_ws_provider.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:encointer_wallet/service/substrate_api/core/dart_api.dart'; @@ -9,8 +10,9 @@ import '../../../utils/test_utils.dart'; void main() { group('encointerDartApi', () { test('gets aggregated account data', () async { - final substrateDartApi = SubstrateDartApi(); - await substrateDartApi.connect('ws://localhost:9944'); + final provider = ReconnectingWsProvider(Uri.parse('ws://localhost:9944')); + + final substrateDartApi = SubstrateDartApi(provider); final encointerDartApi = EncointerDartApi(substrateDartApi); @@ -18,7 +20,7 @@ void main() { // ignore: avoid_print print('data: $data'); - await substrateDartApi.close(); + await provider.disconnect(); }, tags: encointerNodeE2E); }); }