From d7e94bf0dc9926c5c7355ed7cf5bcf7fab84e763 Mon Sep 17 00:00:00 2001 From: Sterling Long Date: Mon, 7 Aug 2023 19:49:52 -0600 Subject: [PATCH] .org fallback, no internet connection error on core start --- lib/apis/core/core.dart | 25 ++++- lib/apis/core/relay_client/relay_client.dart | 41 ++++---- .../websocket/i_websocket_handler.dart | 14 ++- .../websocket/websocket_handler.dart | 55 ++++++----- lib/apis/models/json_rpc_error.dart | 2 +- lib/apis/models/json_rpc_error.freezed.dart | 28 +++--- lib/apis/models/json_rpc_error.g.dart | 7 +- lib/apis/utils/constants.dart | 1 + lib/src/version.dart | 2 +- test/core_api/relay_client_test.dart | 98 ++++++++++++++++++- test/shared/shared_test_utils.dart | 2 + test/shared/shared_test_utils.mocks.dart | 55 ++++++++++- 12 files changed, 252 insertions(+), 78 deletions(-) diff --git a/lib/apis/core/core.dart b/lib/apis/core/core.dart index c74e068c..2e808a33 100644 --- a/lib/apis/core/core.dart +++ b/lib/apis/core/core.dart @@ -15,6 +15,7 @@ import 'package:walletconnect_flutter_v2/apis/core/relay_client/message_tracker. import 'package:walletconnect_flutter_v2/apis/core/relay_client/relay_client.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/http_client.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/i_http_client.dart'; +import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/i_websocket_handler.dart'; import 'package:walletconnect_flutter_v2/apis/core/store/generic_store.dart'; import 'package:walletconnect_flutter_v2/apis/core/store/i_store.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/i_relay_client.dart'; @@ -29,8 +30,9 @@ class Core implements ICore { @override String get version => '2'; + String _relayUrl = WalletConnectConstants.DEFAULT_RELAY_URL; @override - final String relayUrl; + String get relayUrl => _relayUrl; @override final String projectId; @@ -67,12 +69,13 @@ class Core implements ICore { late IStore> storage; Core({ - this.relayUrl = WalletConnectConstants.DEFAULT_RELAY_URL, + relayUrl = WalletConnectConstants.DEFAULT_RELAY_URL, required this.projectId, this.pushUrl = WalletConnectConstants.DEFAULT_PUSH_URL, bool memoryStore = false, Level logLevel = Level.info, IHttpClient httpClient = const HttpWrapper(), + IWebSocketHandler? webSocketHandler, }) { Logger.level = logLevel; storage = SharedPrefsStores( @@ -87,6 +90,7 @@ class Core implements ICore { fromJson: (dynamic value) => value as String, ), ); + relayClient = RelayClient( core: this, messageTracker: MessageTracker( @@ -103,8 +107,9 @@ class Core implements ICore { version: StoreVersions.VERSION_TOPIC_MAP, fromJson: (dynamic value) => value as String, ), - httpClient: httpClient, + socketHandler: webSocketHandler, ); + expirer = Expirer( storage: storage, context: StoreVersions.CONTEXT_EXPIRER, @@ -147,7 +152,19 @@ class Core implements ICore { Future start() async { await storage.init(); await crypto.init(); - await relayClient.init(); + + // If the relay URL is the default, try both it and the backup (.org) + if (relayUrl == WalletConnectConstants.DEFAULT_RELAY_URL) { + _relayUrl = relayUrl; + try { + await relayClient.init(); + } catch (e) { + await relayClient.init(); + } + } else { + await relayClient.init(); + } + await expirer.init(); // await history.init(); await pairing.init(); diff --git a/lib/apis/core/relay_client/relay_client.dart b/lib/apis/core/relay_client/relay_client.dart index ea5a5b6e..5a7d140c 100644 --- a/lib/apis/core/relay_client/relay_client.dart +++ b/lib/apis/core/relay_client/relay_client.dart @@ -8,7 +8,6 @@ import 'package:walletconnect_flutter_v2/apis/core/relay_client/i_message_tracke import 'package:walletconnect_flutter_v2/apis/core/relay_client/i_relay_client.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/json_rpc_2/src/parameters.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/json_rpc_2/src/peer.dart'; -import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/i_http_client.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/i_websocket_handler.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/relay_client_models.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/websocket_handler.dart'; @@ -62,7 +61,7 @@ class RelayClient implements IRelayClient { bool _handledClose = false; // late WebSocketChannel socket; - IWebSocketHandler? socket; + // IWebSocketHandler? socket; Peer? jsonRPC; /// Stores all the subs that haven't been completed @@ -70,7 +69,7 @@ class RelayClient implements IRelayClient { IMessageTracker messageTracker; IGenericStore topicMap; - IHttpClient httpClient; + final IWebSocketHandler socketHandler; ICore core; @@ -81,10 +80,9 @@ class RelayClient implements IRelayClient { required this.core, required this.messageTracker, required this.topicMap, - required this.httpClient, + IWebSocketHandler? socketHandler, this.heartbeatPeriod = 5, - relayUrl = WalletConnectConstants.RELAYER_DEFAULT_PROTOCOL, - }); + }) : socketHandler = socketHandler ?? WebSocketHandler(); @override Future init() async { @@ -196,8 +194,7 @@ class RelayClient implements IRelayClient { await jsonRPC?.close(); jsonRPC = null; - await socket?.close(); - socket = null; + await socketHandler.close(); _heartbeatTimer?.cancel(); _heartbeatTimer = null; @@ -225,21 +222,24 @@ class RelayClient implements IRelayClient { jsonRPC = null; } - if (socket != null) { - await socket!.close(); - socket = null; - } + // if (socket != null) { + // await socket!.close(); + // socket = null; + // } - socket = WebSocketHandler( - url: url, - httpClient: httpClient, - ); + // socket = WebSocketHandler( + // url: url, + // httpClient: httpClient, + // ); core.logger.v('Initializing WebSocket with $url'); - await socket!.init(); + await socketHandler.setup( + url: url, + ); + await socketHandler.connect(); jsonRPC = Peer( - socket!.channel!, + socketHandler.channel!, ); jsonRPC!.registerMethod( @@ -268,7 +268,10 @@ class RelayClient implements IRelayClient { _handledClose = false; jsonRPC!.done.then( (value) { - _handleRelayClose(socket?.closeCode, socket?.closeReason); + _handleRelayClose( + socketHandler.closeCode, + socketHandler.closeReason, + ); }, ); diff --git a/lib/apis/core/relay_client/websocket/i_websocket_handler.dart b/lib/apis/core/relay_client/websocket/i_websocket_handler.dart index b63b1566..7e836f50 100644 --- a/lib/apis/core/relay_client/websocket/i_websocket_handler.dart +++ b/lib/apis/core/relay_client/websocket/i_websocket_handler.dart @@ -3,16 +3,20 @@ import 'dart:async'; import 'package:stream_channel/stream_channel.dart'; abstract class IWebSocketHandler { - abstract final String url; - - Future init(); + String? get url; int? get closeCode; String? get closeReason; StreamChannel? get channel; - Future close(); - Future get ready; + + Future setup({ + required String url, + }); + + Future connect(); + + Future close(); } diff --git a/lib/apis/core/relay_client/websocket/websocket_handler.dart b/lib/apis/core/relay_client/websocket/websocket_handler.dart index 793e306a..5e36c831 100644 --- a/lib/apis/core/relay_client/websocket/websocket_handler.dart +++ b/lib/apis/core/relay_client/websocket/websocket_handler.dart @@ -1,14 +1,14 @@ import 'dart:async'; import 'package:stream_channel/stream_channel.dart'; -import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/i_http_client.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/i_websocket_handler.dart'; +import 'package:walletconnect_flutter_v2/apis/models/basic_models.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class WebSocketHandler implements IWebSocketHandler { + String? _url; @override - final String url; - final IHttpClient httpClient; + String? get url => _url; WebSocketChannel? _socket; @@ -24,31 +24,32 @@ class WebSocketHandler implements IWebSocketHandler { @override Future get ready => _socket!.ready; - WebSocketHandler({ - required this.url, - required this.httpClient, - }); + // const WebSocketHandler(); @override - Future init() async { - await _connect(); - } + Future setup({ + required String url, + }) async { + _url = url; - @override - Future close() async { - try { - await _socket?.sink.close(); - } catch (_) {} - _socket = null; + await close(); } - Future _connect() async { + @override + Future connect() async { // print('connecting'); - _socket = WebSocketChannel.connect( - Uri.parse( - '$url&useOnCloseEvent=true', - ), - ); + try { + _socket = WebSocketChannel.connect( + Uri.parse( + '$url&useOnCloseEvent=true', + ), + ); + } catch (e) { + throw WalletConnectError( + code: -1, + message: 'No internet connection: ${e.toString()}', + ); + } _channel = _socket!.cast(); @@ -72,6 +73,16 @@ class WebSocketHandler implements IWebSocketHandler { // } } + @override + Future close() async { + try { + if (_socket != null) { + await _socket?.sink.close(); + } + } catch (_) {} + _socket = null; + } + @override String toString() { return 'WebSocketHandler{url: $url, _socket: $_socket, _channel: $_channel}'; diff --git a/lib/apis/models/json_rpc_error.dart b/lib/apis/models/json_rpc_error.dart index df98fc5a..af067cf0 100644 --- a/lib/apis/models/json_rpc_error.dart +++ b/lib/apis/models/json_rpc_error.dart @@ -7,7 +7,7 @@ part 'json_rpc_error.freezed.dart'; class JsonRpcError with _$JsonRpcError { @JsonSerializable(includeIfNull: false) const factory JsonRpcError({ - required int code, + int? code, String? message, }) = _JsonRpcError; diff --git a/lib/apis/models/json_rpc_error.freezed.dart b/lib/apis/models/json_rpc_error.freezed.dart index 101dc78e..5163ff4e 100644 --- a/lib/apis/models/json_rpc_error.freezed.dart +++ b/lib/apis/models/json_rpc_error.freezed.dart @@ -20,7 +20,7 @@ JsonRpcError _$JsonRpcErrorFromJson(Map json) { /// @nodoc mixin _$JsonRpcError { - int get code => throw _privateConstructorUsedError; + int? get code => throw _privateConstructorUsedError; String? get message => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -35,7 +35,7 @@ abstract class $JsonRpcErrorCopyWith<$Res> { JsonRpcError value, $Res Function(JsonRpcError) then) = _$JsonRpcErrorCopyWithImpl<$Res, JsonRpcError>; @useResult - $Res call({int code, String? message}); + $Res call({int? code, String? message}); } /// @nodoc @@ -51,14 +51,14 @@ class _$JsonRpcErrorCopyWithImpl<$Res, $Val extends JsonRpcError> @pragma('vm:prefer-inline') @override $Res call({ - Object? code = null, + Object? code = freezed, Object? message = freezed, }) { return _then(_value.copyWith( - code: null == code + code: freezed == code ? _value.code : code // ignore: cast_nullable_to_non_nullable - as int, + as int?, message: freezed == message ? _value.message : message // ignore: cast_nullable_to_non_nullable @@ -75,7 +75,7 @@ abstract class _$$_JsonRpcErrorCopyWith<$Res> __$$_JsonRpcErrorCopyWithImpl<$Res>; @override @useResult - $Res call({int code, String? message}); + $Res call({int? code, String? message}); } /// @nodoc @@ -89,14 +89,14 @@ class __$$_JsonRpcErrorCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? code = null, + Object? code = freezed, Object? message = freezed, }) { return _then(_$_JsonRpcError( - code: null == code + code: freezed == code ? _value.code : code // ignore: cast_nullable_to_non_nullable - as int, + as int?, message: freezed == message ? _value.message : message // ignore: cast_nullable_to_non_nullable @@ -109,13 +109,13 @@ class __$$_JsonRpcErrorCopyWithImpl<$Res> @JsonSerializable(includeIfNull: false) class _$_JsonRpcError implements _JsonRpcError { - const _$_JsonRpcError({required this.code, this.message}); + const _$_JsonRpcError({this.code, this.message}); factory _$_JsonRpcError.fromJson(Map json) => _$$_JsonRpcErrorFromJson(json); @override - final int code; + final int? code; @override final String? message; @@ -152,14 +152,14 @@ class _$_JsonRpcError implements _JsonRpcError { } abstract class _JsonRpcError implements JsonRpcError { - const factory _JsonRpcError( - {required final int code, final String? message}) = _$_JsonRpcError; + const factory _JsonRpcError({final int? code, final String? message}) = + _$_JsonRpcError; factory _JsonRpcError.fromJson(Map json) = _$_JsonRpcError.fromJson; @override - int get code; + int? get code; @override String? get message; @override diff --git a/lib/apis/models/json_rpc_error.g.dart b/lib/apis/models/json_rpc_error.g.dart index 41ea545d..953788b3 100644 --- a/lib/apis/models/json_rpc_error.g.dart +++ b/lib/apis/models/json_rpc_error.g.dart @@ -8,14 +8,12 @@ part of 'json_rpc_error.dart'; _$_JsonRpcError _$$_JsonRpcErrorFromJson(Map json) => _$_JsonRpcError( - code: json['code'] as int, + code: json['code'] as int?, message: json['message'] as String?, ); Map _$$_JsonRpcErrorToJson(_$_JsonRpcError instance) { - final val = { - 'code': instance.code, - }; + final val = {}; void writeNotNull(String key, dynamic value) { if (value != null) { @@ -23,6 +21,7 @@ Map _$$_JsonRpcErrorToJson(_$_JsonRpcError instance) { } } + writeNotNull('code', instance.code); writeNotNull('message', instance.message); return val; } diff --git a/lib/apis/utils/constants.dart b/lib/apis/utils/constants.dart index e20e669d..9f1d859c 100644 --- a/lib/apis/utils/constants.dart +++ b/lib/apis/utils/constants.dart @@ -6,6 +6,7 @@ class WalletConnectConstants { static const CORE_CONTEXT = 'core'; static const DEFAULT_RELAY_URL = 'wss://relay.walletconnect.com'; + static const FALLBACK_RELAY_URL = 'wss://relay.walletconnect.org'; static const CORE_STORAGE_PREFIX = '$CORE_PROTOCOL@$CORE_VERSION:$CORE_CONTEXT:'; diff --git a/lib/src/version.dart b/lib/src/version.dart index 80cdf523..399bbe95 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '2.0.13'; +const packageVersion = '2.0.16'; diff --git a/test/core_api/relay_client_test.dart b/test/core_api/relay_client_test.dart index ac97fcf2..d9d82d2e 100644 --- a/test/core_api/relay_client_test.dart +++ b/test/core_api/relay_client_test.dart @@ -28,6 +28,101 @@ void main() { }); group('Relay throws errors', () { + test('on init if there is no internet connection', () async { + final MockWebSocketHandler mockWebSocketHandler = MockWebSocketHandler(); + when(mockWebSocketHandler.setup(url: anyNamed('url'))).thenAnswer( + (_) async => Future.value(), + ); + when(mockWebSocketHandler.connect()).thenThrow(const WalletConnectError( + code: -1, + message: 'No internet connection: test', + )); + + ICore core = Core( + projectId: 'abc', + memoryStore: true, + ); + core.relayClient = RelayClient( + core: core, + messageTracker: getMessageTracker(core: core), + topicMap: getTopicMap(core: core), + socketHandler: mockWebSocketHandler, + ); + + expect( + () async => await core.start(), + throwsA( + isA().having( + (e) => e.message, + 'No internet connection', + 'No internet connection: test', + ), + ), + ); + + // Check that setup was called twice (It attempts the fallback .org URL if the default URL is provided) + // TODO: Figure out why the mocked class isn't counting the number of times it's called + // verify(mockWebSocketHandler.setup(url: anyNamed('url'))).called(2); + // verify(mockWebSocketHandler.connect()).called(2); + + core = Core( + projectId: 'abc', + memoryStore: true, + relayUrl: 'wss://relay.test.com', + ); + core.relayClient = RelayClient( + core: core, + messageTracker: getMessageTracker(core: core), + topicMap: getTopicMap(core: core), + socketHandler: mockWebSocketHandler, + ); + + expect( + () async => await core.start(), + throwsA( + isA().having( + (e) => e.message, + 'No internet connection', + 'No internet connection: test', + ), + ), + ); + + // // Check that setup was called once for custom URL + // TODO: Figure out why the mocked class isn't counting the number of times it's called + // verify(mockWebSocketHandler.setup(url: anyNamed('url'))).called(1); + }); + + test('on init if the url fails', () async { + final MockWebSocketHandler mockWebSocketHandler = MockWebSocketHandler(); + when(mockWebSocketHandler.connect()).thenThrow(const WalletConnectError( + code: -1, + message: 'No internet connection: test', + )); + + final ICore core = Core( + projectId: 'abc', + memoryStore: true, + ); + core.relayClient = RelayClient( + core: core, + messageTracker: getMessageTracker(core: core), + topicMap: getTopicMap(core: core), + socketHandler: mockWebSocketHandler, + ); + + expect( + () async => await core.start(), + throwsA( + isA().having( + (e) => e.message, + 'No internet connection', + 'No internet connection: test', + ), + ), + ); + }); + test('when connection parameters are invalid', () async { final http = MockHttpWrapper(); when(http.get(any)).thenAnswer( @@ -165,7 +260,6 @@ void main() { core: core, messageTracker: messageTracker, topicMap: getTopicMap(core: core), - httpClient: getHttpWrapper(), ); await relayClient.init(); }); @@ -227,13 +321,11 @@ void main() { core: coreA, messageTracker: getMessageTracker(core: coreA), topicMap: getTopicMap(core: coreA), - httpClient: getHttpWrapper(), ); coreB.relayClient = RelayClient( core: coreB, messageTracker: getMessageTracker(core: coreB), topicMap: getTopicMap(core: coreB), - httpClient: getHttpWrapper(), ); await coreA.relayClient.init(); await coreB.relayClient.init(); diff --git a/test/shared/shared_test_utils.dart b/test/shared/shared_test_utils.dart index 492aea31..c9b56aa9 100644 --- a/test/shared/shared_test_utils.dart +++ b/test/shared/shared_test_utils.dart @@ -11,6 +11,7 @@ import 'package:walletconnect_flutter_v2/apis/core/i_core.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/i_message_tracker.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/message_tracker.dart'; import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/http_client.dart'; +import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/websocket_handler.dart'; import 'package:walletconnect_flutter_v2/apis/core/store/generic_store.dart'; import 'package:walletconnect_flutter_v2/apis/core/store/i_generic_store.dart'; import 'package:walletconnect_flutter_v2/apis/utils/constants.dart'; @@ -24,6 +25,7 @@ import 'shared_test_utils.mocks.dart'; MessageTracker, HttpWrapper, Core, + WebSocketHandler, ]) class SharedTestUtils {} diff --git a/test/shared/shared_test_utils.mocks.dart b/test/shared/shared_test_utils.mocks.dart index 07906bd2..772acccc 100644 --- a/test/shared/shared_test_utils.mocks.dart +++ b/test/shared/shared_test_utils.mocks.dart @@ -34,6 +34,8 @@ import 'package:walletconnect_flutter_v2/apis/core/relay_client/message_tracker. as _i20; import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/http_client.dart' as _i22; +import 'package:walletconnect_flutter_v2/apis/core/relay_client/websocket/websocket_handler.dart' + as _i24; import 'package:walletconnect_flutter_v2/apis/core/store/i_generic_store.dart' as _i4; import 'package:walletconnect_flutter_v2/apis/core/store/i_store.dart' as _i7; @@ -922,11 +924,6 @@ class MockCore extends _i1.Mock implements _i23.Core { _i1.throwOnMissingStub(this); } - @override - String get relayUrl => (super.noSuchMethod( - Invocation.getter(#relayUrl), - returnValue: '', - ) as String); @override String get projectId => (super.noSuchMethod( Invocation.getter(#projectId), @@ -1052,6 +1049,11 @@ class MockCore extends _i1.Mock implements _i23.Core { returnValue: '', ) as String); @override + String get relayUrl => (super.noSuchMethod( + Invocation.getter(#relayUrl), + returnValue: '', + ) as String); + @override _i18.Future start() => (super.noSuchMethod( Invocation.method( #start, @@ -1061,3 +1063,46 @@ class MockCore extends _i1.Mock implements _i23.Core { returnValueForMissingStub: _i18.Future.value(), ) as _i18.Future); } + +/// A class which mocks [WebSocketHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebSocketHandler extends _i1.Mock implements _i24.WebSocketHandler { + MockWebSocketHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i18.Future get ready => (super.noSuchMethod( + Invocation.getter(#ready), + returnValue: _i18.Future.value(), + ) as _i18.Future); + @override + _i18.Future setup({required String? url}) => (super.noSuchMethod( + Invocation.method( + #setup, + [], + {#url: url}, + ), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) as _i18.Future); + @override + _i18.Future connect() => (super.noSuchMethod( + Invocation.method( + #connect, + [], + ), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) as _i18.Future); + @override + _i18.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) as _i18.Future); +}