diff --git a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/Wc2ConnectPlugin.kt b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/Wc2ConnectPlugin.kt index e03152e30..d45c5aa0d 100644 --- a/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/Wc2ConnectPlugin.kt +++ b/android/app/src/main/kotlin/com/bitmark/autonomy_flutter/Wc2ConnectPlugin.kt @@ -126,7 +126,10 @@ class Wc2ConnectPlugin(private val application: Application) : FlutterPlugin, override fun onSessionProposal(sessionProposal: Sign.Model.SessionProposal) { Timber.d("[WalletDelegate] onSessionProposal $sessionProposal") pendingProposals.add(sessionProposal) - val namespaces = sessionProposal.requiredNamespaces.mapValues { e -> + val requiredNamespaces = sessionProposal.requiredNamespaces.mapValues { e -> + e.value.toProposalNamespace() + } + val optionalNamespaces = sessionProposal.optionalNamespaces.mapValues { e -> e.value.toProposalNamespace() } val proposer = mapOf( @@ -138,7 +141,8 @@ class Wc2ConnectPlugin(private val application: Application) : FlutterPlugin, val params = mapOf( "id" to sessionProposal.proposerPublicKey, "proposer" to Gson().toJson(proposer), - "requiredNamespaces" to Json.encodeToString(namespaces) + "requiredNamespaces" to Json.encodeToString(requiredNamespaces), + "optionalNamespaces" to Json.encodeToString(optionalNamespaces) ) mainScope?.launch { eventPublisher.emit( @@ -247,7 +251,7 @@ class Wc2ConnectPlugin(private val application: Application) : FlutterPlugin, result.error("-1", "Proposal not found", null) return } - val namespaces = proposal.requiredNamespaces.mapValues { + val namespaces = (proposal.optionalNamespaces + proposal.requiredNamespaces).mapValues { Sign.Model.Namespace.Session( chains = it.value.chains, methods = it.value.methods, @@ -255,6 +259,7 @@ class Wc2ConnectPlugin(private val application: Application) : FlutterPlugin, accounts = it.value.chains?.map { chain -> "$chain:$account" } ?: emptyList(), ) } + Timber.d("Approve namespace: $namespaces") try { SignClient.approveSession( Sign.Params.Approve( diff --git a/ios/Runner/Wallet Connect 2.0/WC2ChannelHanler.swift b/ios/Runner/Wallet Connect 2.0/WC2ChannelHanler.swift index 755ae6a75..1f7d1ccce 100644 --- a/ios/Runner/Wallet Connect 2.0/WC2ChannelHanler.swift +++ b/ios/Runner/Wallet Connect 2.0/WC2ChannelHanler.swift @@ -124,8 +124,16 @@ class WC2ChannelHandler: NSObject { return } + var allNamespaces = proposal.requiredNamespaces + + if let optionalNamespaces = proposal.optionalNamespaces { + allNamespaces.merge(optionalNamespaces) { requiredNamespaces, _ in + return requiredNamespaces + } + } + var sessionNamespaces = [String: SessionNamespace]() - proposal.requiredNamespaces.forEach { + allNamespaces.forEach { let caip2Namespace = $0.key let proposalNamespace = $0.value let accounts = Set(proposalNamespace.chains!.compactMap { Account($0.absoluteString + ":\(account)") }) @@ -165,10 +173,12 @@ extension WC2ChannelHandler: FlutterStreamHandler { var params: [String: Any] = [:] let proposer = try? JSONEncoder().encode(sessionProposal.proposer) - let namespaces = try? JSONEncoder().encode(sessionProposal.requiredNamespaces) + let requiredNamespaces = try? JSONEncoder().encode(sessionProposal.requiredNamespaces) + let optionalNamespaces = try? JSONEncoder().encode(sessionProposal.optionalNamespaces) params["id"] = sessionProposal.id params["proposer"] = proposer != nil ? String(data: proposer!, encoding: .utf8) : nil - params["requiredNamespaces"] = namespaces != nil ? String(data: namespaces!, encoding: .utf8) : nil + params["requiredNamespaces"] = requiredNamespaces != nil ? String(data: requiredNamespaces!, encoding: .utf8) : nil + params["optionalNamespaces"] = optionalNamespaces != nil ? String(data: optionalNamespaces!, encoding: .utf8) : nil events([ "eventName": "onSessionProposal", diff --git a/lib/model/connection_request_args.dart b/lib/model/connection_request_args.dart index f64ae20ff..9c6a2fbab 100644 --- a/lib/model/connection_request_args.dart +++ b/lib/model/connection_request_args.dart @@ -5,8 +5,7 @@ // that can be found in the LICENSE file. // -import 'package:autonomy_flutter/service/wc2_service.dart'; -import 'package:collection/collection.dart'; +import 'package:autonomy_flutter/model/wc2_request.dart'; import 'package:tezart/tezart.dart'; abstract class ConnectionRequest { @@ -16,7 +15,7 @@ abstract class ConnectionRequest { bool get isBeaconConnect => false; - get id; + String get id; String? get name; @@ -42,7 +41,7 @@ class BeaconRequest extends ConnectionRequest { bool get isBeaconConnect => true; @override - get id => _id; + String get id => _id; @override String? get name => appName; @@ -70,6 +69,7 @@ class Wc2Proposal extends ConnectionRequest { this._id, { required this.proposer, required this.requiredNamespaces, + this.optionalNamespaces = const {}, }); @override @@ -79,19 +79,20 @@ class Wc2Proposal extends ConnectionRequest { bool get isAutonomyConnect => _isAutonomyConnect(); bool _isAutonomyConnect() { - final proposalMethods = - requiredNamespaces.values.map((e) => e.methods).flattened.toSet(); - final unsupportedMethods = - proposalMethods.difference(Wc2Service.autonomyMethods); - return unsupportedMethods.isEmpty; + final proposalChains = allNamespaces.keys.toSet(); + return proposalChains.contains(Wc2Chain.autonomy); } AppMetadata proposer; Map requiredNamespaces; + Map optionalNamespaces; + + Map get allNamespaces => + {...requiredNamespaces, ...optionalNamespaces}; final String _id; @override - get id => _id; + String get id => _id; @override String? get name => proposer.name; @@ -114,17 +115,17 @@ class AppMetadata { String description; factory AppMetadata.fromJson(Map json) => AppMetadata( - icons: List.from(json["icons"].map((x) => x)), - name: json["name"], - url: json["url"], - description: json["description"], + icons: List.from(json['icons'].map((x) => x)), + name: json['name'], + url: json['url'], + description: json['description'], ); Map toJson() => { - "icons": List.from(icons.map((x) => x)), - "name": name, - "url": url, - "description": description, + 'icons': List.from(icons.map((x) => x)), + 'name': name, + 'url': url, + 'description': description, }; } @@ -140,14 +141,14 @@ class Wc2Namespace { List events; factory Wc2Namespace.fromJson(Map json) => Wc2Namespace( - chains: List.from(json["chains"].map((x) => x)), - methods: List.from(json["methods"].map((x) => x)), - events: List.from(json["events"].map((x) => x)), + chains: List.from(json['chains'].map((x) => x)), + methods: List.from(json['methods'].map((x) => x)), + events: List.from(json['events'].map((x) => x)), ); Map toJson() => { - "chains": List.from(chains.map((x) => x)), - "methods": List.from(methods.map((x) => x)), - "events": List.from(events.map((x) => x)), + 'chains': List.from(chains.map((x) => x)), + 'methods': List.from(methods.map((x) => x)), + 'events': List.from(events.map((x) => x)), }; } diff --git a/lib/service/wc2_service.dart b/lib/service/wc2_service.dart index 0bce16af0..675b72ac0 100644 --- a/lib/service/wc2_service.dart +++ b/lib/service/wc2_service.dart @@ -9,6 +9,8 @@ import 'dart:async'; import 'dart:convert'; import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/database/cloud_database.dart'; +import 'package:autonomy_flutter/database/entity/connection.dart'; import 'package:autonomy_flutter/model/connection_request_args.dart'; import 'package:autonomy_flutter/model/wc2_pairing.dart'; import 'package:autonomy_flutter/model/wc2_request.dart'; @@ -30,9 +32,6 @@ import 'package:autonomy_flutter/util/wc2_tezos_ext.dart'; import 'package:collection/collection.dart'; import 'package:web3dart/credentials.dart'; -import '../database/cloud_database.dart'; -import '../database/entity/connection.dart'; - class Wc2Service extends Wc2Handler { static final Set _supportedChains = { Wc2Chain.autonomy, @@ -41,21 +40,21 @@ class Wc2Service extends Wc2Handler { }; static final Set supportedMethods = { - "au_sign", - "au_permissions", - "au_sendTransaction", - "eth_sendTransaction", - "personal_sign", - "eth_sign", - "eth_signTypedData", - "eth_signTypedData_v4", - "eth_signTransaction" + 'au_sign', + 'au_permissions', + 'au_sendTransaction', + 'eth_sendTransaction', + 'personal_sign', + 'eth_sign', + 'eth_signTypedData', + 'eth_signTypedData_v4', + 'eth_signTransaction' }; static final Set autonomyMethods = { - "au_sign", - "au_permissions", - "au_sendTransaction", + 'au_sign', + 'au_permissions', + 'au_sendTransaction', }; final NavigationService _navigationService; @@ -63,7 +62,7 @@ class Wc2Service extends Wc2Handler { final CloudDatabase _cloudDB; late Wc2Channel _wc2channel; - String pendingUri = ""; + String pendingUri = ''; Timer? _timer; @@ -83,7 +82,9 @@ class Wc2Service extends Wc2Handler { } String? getFirstSession() { - if (_pendingSessions.isEmpty) return null; + if (_pendingSessions.isEmpty) { + return null; + } return _pendingSessions.first; } @@ -114,18 +115,18 @@ class Wc2Service extends Wc2Handler { } final ids = connections - .map((e) => e.key.split(":").lastOrNull) + .map((e) => e.key.split(':').lastOrNull) .whereNotNull() .toList(); - _wc2channel.cleanup(ids); + unawaited(_wc2channel.cleanup(ids)); } Future approveSession(Wc2Proposal proposal, {required String account, required String connectionKey, required String accountNumber, - isAuConnect = false}) async { + bool isAuConnect = false}) async { await _wc2channel.approve( proposal.id, account, @@ -134,10 +135,10 @@ class Wc2Service extends Wc2Handler { final topic = wc2Pairings .firstWhereOrNull((element) => pendingUri.contains(element.topic)) ?.topic ?? - ""; + ''; final connection = Connection( - key: "$connectionKey:$topic", + key: '$connectionKey:$topic', name: proposal.proposer.name, data: json.encode(proposal.proposer), connectionType: isAuConnect @@ -163,7 +164,7 @@ class Wc2Service extends Wc2Handler { } Future respondOnApprove(String topic, String response) async { - log.info("[Wc2Service] respondOnApprove topic $topic, response: $response"); + log.info('[Wc2Service] respondOnApprove topic $topic, response: $response'); await _wc2channel.respondOnApprove(topic, response); } @@ -171,7 +172,7 @@ class Wc2Service extends Wc2Handler { String topic, { String? reason, }) async { - log.info("[Wc2Service] respondOnReject topic $topic, reason: $reason"); + log.info('[Wc2Service] respondOnReject topic $topic, reason: $reason'); await _wc2channel.respondOnReject( topic: topic, reason: reason, @@ -179,12 +180,11 @@ class Wc2Service extends Wc2Handler { } //#region Pairing - Future> getPairings() async { - return await _wc2channel.getPairings(); - } + Future> getPairings() async => + await _wc2channel.getPairings(); Future deletePairing({required String topic}) async { - log.info("[Wc2Service] Delete pairing. Topic: $topic"); + log.info('[Wc2Service] Delete pairing. Topic: $topic'); return await _wc2channel.deletePairing(topic: topic); } @@ -193,13 +193,13 @@ class Wc2Service extends Wc2Handler { //#region Events handling @override void onSessionProposal(Wc2Proposal proposal) async { - log.info("[WC2Service] onSessionProposal: id = ${proposal.id}"); + log.info('[WC2Service] onSessionProposal: id = ${proposal.id}'); _timer?.cancel(); final unsupportedChains = proposal.requiredNamespaces.keys.toSet().difference(_supportedChains); if (unsupportedChains.isNotEmpty) { - log.info("[Wc2Service] Proposal contains unsupported chains: " - "$unsupportedChains"); + log.info('[Wc2Service] Proposal contains unsupported chains: ' + '$unsupportedChains'); await rejectSession( proposal.id, reason: "Chains ${unsupportedChains.join(", ")} not supported", @@ -213,17 +213,17 @@ class Wc2Service extends Wc2Handler { .toSet(); final unsupportedMethods = proposalMethods.difference(supportedMethods); if (unsupportedMethods.isNotEmpty) { - log.info("[Wc2Service] Proposal contains unsupported methods: " - "$unsupportedMethods"); + log.info('[Wc2Service] Proposal contains unsupported methods: ' + '$unsupportedMethods'); } - _navigationService.navigateTo(AppRouter.wc2ConnectPage, - arguments: proposal); + unawaited(_navigationService.navigateTo(AppRouter.wc2ConnectPage, + arguments: proposal)); } @override void onSessionRequest(Wc2Request request) async { switch (request.method) { - case "au_sign": + case 'au_sign': switch (request.chainId.caip2Namespace) { case Wc2Chain.ethereum: await _handleEthereumSignRequest( @@ -236,62 +236,62 @@ class Wc2Service extends Wc2Handler { await _handleAutonomySignRequest(request); break; default: - log.info("[Wc2Service] Unsupported chain: ${request.method}"); + log.info('[Wc2Service] Unsupported chain: ${request.method}'); await respondOnReject( request.topic, - reason: "Chain ${request.chainId} is not supported", + reason: 'Chain ${request.chainId} is not supported', ); } break; - case "au_permissions": - _navigationService.navigateTo( + case 'au_permissions': + unawaited(_navigationService.navigateTo( AppRouter.wc2PermissionPage, arguments: request, - ); + )); break; - case "au_sendTransaction": - final chain = request.params["chain"] as String; + case 'au_sendTransaction': + final chain = request.params['chain'] as String; switch (chain.caip2Namespace) { case Wc2Chain.ethereum: - _handleEthereumSendTransactionRequest(request); + unawaited(_handleEthereumSendTransactionRequest(request)); break; case Wc2Chain.tezos: try { final beaconReq = request.toBeaconRequest(); - _navigationService.navigateTo( + unawaited(_navigationService.navigateTo( TBSendTransactionPage.tag, arguments: beaconReq, - ); + )); } catch (e) { - await respondOnReject(request.topic, reason: "$e"); + await respondOnReject(request.topic, reason: '$e'); } break; default: await respondOnReject( request.topic, - reason: "Chain $chain is not supported", + reason: 'Chain $chain is not supported', ); } break; - case "eth_sendTransaction": + case 'eth_sendTransaction': await _handleWC2EthereumSendTransactionRequest(request); break; - case "personal_sign": + case 'personal_sign': await _handleWC2EthereumSignRequest( request, WCSignType.PERSONAL_MESSAGE); break; - case "eth_signTypedData": - case "eth_signTypedData_v4": + case 'eth_signTypedData': + case 'eth_signTypedData_v4': await _handleWC2EthereumSignRequest(request, WCSignType.TYPED_MESSAGE); break; - case "eth_sign": + case 'eth_sign': await _handleWC2EthereumSignRequest(request, WCSignType.MESSAGE); break; default: - log.info("[Wc2Service] Unsupported method: ${request.method}"); + log.info('[Wc2Service] Unsupported method: ${request.method}'); await respondOnReject( request.topic, - reason: "Method ${request.method} is not supported", + reason: 'Method ${request.method} is not supported', ); } } @@ -299,7 +299,8 @@ class Wc2Service extends Wc2Handler { //#endregion Future _handleWC2EthereumSignRequest( Wc2Request request, WCSignType signType) async { - String address, message; + String address; + String message; if (signType == WCSignType.PERSONAL_MESSAGE) { address = request.params[1]; message = request.params[0]; @@ -314,7 +315,7 @@ class Wc2Service extends Wc2Handler { final eip55address = EthereumAddress.fromHex(address).hexEip55; final wallet = await _accountService.getAccountByAddress( - chain: "eip155", + chain: 'eip155', address: eip55address, ); await _navigationService.navigateTo(WCSignMessagePage.tag, @@ -334,25 +335,29 @@ class Wc2Service extends Wc2Handler { try { var transaction = request.params[0] as Map; final eip55address = - EthereumAddress.fromHex(transaction["from"]).hexEip55; + EthereumAddress.fromHex(transaction['from']).hexEip55; final walletIndex = await _accountService.getAccountByAddress( - chain: "eip155", + chain: 'eip155', address: eip55address, ); - if (transaction["data"] == null) transaction["data"] = ""; - if (transaction["gas"] == null) transaction["gas"] = ""; - if (transaction["to"] == null) { - log.info("[Wc2Service] Invalid transaction: no recipient"); + if (transaction['data'] == null) { + transaction['data'] = ''; + } + if (transaction['gas'] == null) { + transaction['gas'] = ''; + } + if (transaction['to'] == null) { + log.info('[Wc2Service] Invalid transaction: no recipient'); await respondOnReject( request.topic, - reason: "Invalid transaction: no recipient", + reason: 'Invalid transaction: no recipient', ); return; } final metaData = request.proposer != null ? request.proposer! - : AppMetadata(icons: [], name: "", url: "", description: ""); + : AppMetadata(icons: [], name: '', url: '', description: ''); final args = WCSendTransactionPageArgs( request.id, metaData, @@ -362,12 +367,12 @@ class Wc2Service extends Wc2Handler { topic: request.topic, isWalletConnect2: true, ); - _navigationService.navigateTo( + unawaited(_navigationService.navigateTo( WCSendTransactionPage.tag, arguments: args, - ); + )); } catch (e) { - await respondOnReject(request.topic, reason: "$e"); + await respondOnReject(request.topic, reason: '$e'); } } @@ -379,15 +384,15 @@ class Wc2Service extends Wc2Handler { request.id, request.topic, request.proposer!, - request.params["message"], + request.params['message'], signType, - "", + '', 0, // uuid, index, used for Wallet connect 1 only wc2Params: Wc2SignRequestParams( - chain: request.params["chain"], - address: request.params["address"], - message: request.params["message"], + chain: request.params['chain'], + address: request.params['address'], + message: request.params['message'], ), )); } @@ -410,24 +415,28 @@ class Wc2Service extends Wc2Handler { Future _handleEthereumSendTransactionRequest(Wc2Request request) async { try { final walletIndex = await _accountService.getAccountByAddress( - chain: "eip155", - address: request.params["address"], + chain: 'eip155', + address: request.params['address'], ); var transaction = - request.params["transactions"][0] as Map; - if (transaction["data"] == null) transaction["data"] = ""; - if (transaction["gas"] == null) transaction["gas"] = ""; - if (transaction["to"] == null) { - log.info("[Wc2Service] Invalid transaction: no recipient"); + request.params['transactions'][0] as Map; + if (transaction['data'] == null) { + transaction['data'] = ''; + } + if (transaction['gas'] == null) { + transaction['gas'] = ''; + } + if (transaction['to'] == null) { + log.info('[Wc2Service] Invalid transaction: no recipient'); await respondOnReject( request.topic, - reason: "Invalid transaction: no recipient", + reason: 'Invalid transaction: no recipient', ); return; } final metaData = request.proposer != null ? request.proposer! - : AppMetadata(icons: [], name: "", url: "", description: ""); + : AppMetadata(icons: [], name: '', url: '', description: ''); final args = WCSendTransactionPageArgs( request.id, metaData, @@ -437,12 +446,12 @@ class Wc2Service extends Wc2Handler { topic: request.topic, isWalletConnect2: true, ); - _navigationService.navigateTo( + unawaited(_navigationService.navigateTo( WCSendTransactionPage.tag, arguments: args, - ); + )); } catch (e) { - await respondOnReject(request.topic, reason: "$e"); + await respondOnReject(request.topic, reason: '$e'); } } //#endregion diff --git a/lib/util/wc2_channel.dart b/lib/util/wc2_channel.dart index 89085b444..39f6a1d2f 100644 --- a/lib/util/wc2_channel.dart +++ b/lib/util/wc2_channel.dart @@ -5,11 +5,12 @@ // that can be found in the LICENSE file. // +import 'dart:async'; import 'dart:convert'; +import 'package:autonomy_flutter/model/connection_request_args.dart'; import 'package:autonomy_flutter/model/wc2_pairing.dart'; import 'package:autonomy_flutter/model/wc2_request.dart'; -import 'package:autonomy_flutter/model/connection_request_args.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:flutter/services.dart'; @@ -19,7 +20,7 @@ class Wc2Channel { EventChannel('wallet_connect_v2/event'); Wc2Channel({required this.handler}) { - listen(); + unawaited(listen()); } Wc2Handler? handler; @@ -59,22 +60,22 @@ class Wc2Channel { String? reason, }) async { await _channel.invokeMethod('respondOnReject', { - "topic": topic, + 'topic': topic, if (reason?.isNotEmpty == true) ...{ - "reason": reason, + 'reason': reason, } }); } Future> getPairings() async { - final jsonString = await _channel.invokeMethod("getPairings"); + final jsonString = await _channel.invokeMethod('getPairings'); final json = jsonDecode(jsonString) as List; return json.map((e) => Wc2Pairing.fromJson(e)).toList(); } Future deletePairing({required String topic}) async { - await _channel.invokeMethod("deletePairing", { - "topic": topic, + await _channel.invokeMethod('deletePairing', { + 'topic': topic, }); } @@ -82,40 +83,47 @@ class Wc2Channel { await _channel.invokeMethod('cleanup', {'retain_ids': ids}); } - void listen() async { + Future listen() async { await for (Map event in _eventChannel.receiveBroadcastStream()) { - var params = event["params"]; - switch (event["eventName"]) { - case "onConnected": - log.info("[WC2Channel] onConnected"); + var params = event['params']; + switch (event['eventName']) { + case 'onConnected': + log.info('[WC2Channel] onConnected'); break; - case "onSessionProposal": - log.info("[WC2Channel] onSessionProposal"); - final id = params["id"]; + case 'onSessionProposal': + log.info('[WC2Channel] onSessionProposal'); + final id = params['id']; final proposer = - AppMetadata.fromJson(json.decode(params["proposer"])); - final Map requiredNamespaces = - json.decode(params["requiredNamespaces"]); - final namespaces = requiredNamespaces - .map((key, value) => MapEntry(key, Wc2Namespace.fromJson(value))); + AppMetadata.fromJson(json.decode(params['proposer'])); + final Map requiredNamespacesJson = + json.decode(params['requiredNamespaces']); + final Map requiredNamespaces = + requiredNamespacesJson.map( + (key, value) => MapEntry(key, Wc2Namespace.fromJson(value))); + final Map optionalNamespacesJson = + json.decode(params['optionalNamespaces']); + final Map optionalNamespaces = + optionalNamespacesJson.map( + (key, value) => MapEntry(key, Wc2Namespace.fromJson(value))); final request = Wc2Proposal( id, proposer: proposer, - requiredNamespaces: namespaces, + requiredNamespaces: requiredNamespaces, + optionalNamespaces: optionalNamespaces, ); handler?.onSessionProposal(request); break; - case "onSessionSettle": - log.info("[WC2Channel] onSessionSettle"); + case 'onSessionSettle': + log.info('[WC2Channel] onSessionSettle'); break; - case "onSessionRequest": - log.info("[WC2Channel] onSessionRequest"); + case 'onSessionRequest': + log.info('[WC2Channel] onSessionRequest'); log.info(params); final request = Wc2Request.fromJson(json.decode(params)); handler?.onSessionRequest(request); break; - case "onSessionDelete": - log.info("[WC2Channel] onSessionDelete"); + case 'onSessionDelete': + log.info('[WC2Channel] onSessionDelete'); break; } }