reown-com/reown_flutter

Bad state: No element

Closed this issue · 10 comments

Describe the bug

Method getNetworkById thrown an error when app running in flutterMode: release
In debug mode, all works fine!

Error:

flutter: Bad state: No element

Method:

static ReownAppKitModalNetworkInfo? getNetworkById(
    String namespace,
    String chainId,
  ) {
    return getAllSupportedNetworks(namespace: namespace).firstWhere(
      (e) => e.chainId == chainId,
    );
  }

SDK version:

reown_appkit: ^1.2.0-alpha01

@quetool Can you please check and advise?

It's tough to find a source of the issue because of the release mode. I hope you will find some time to check it.

Also, under from init method I received an error too

LateInitializationError: Field '_bundleId@3327041263' has already been initialized.

Hello @damienissa could you provide a minimum reproducible code of both issues?

It's tough to find a source of the issue because of the release mode. I hope you will find some time to check it.

This is why profile mode exists. You should be able to run your code in profile mode, which is kind of release but with logging.

@quetool
This is my service for working with the appKit.
Also, I can create a simple project with this service and place it on github repo.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:injectable/injectable.dart';
import 'package:reown_appkit/reown_appkit.dart' hide ReownConstants;

import 'networks.dart';
import 'reown_constants.dart';

abstract interface class IReownService {
  bool get isConnected;
  void init({required BuildContext context});
  Stream<DateTime> get onChanged;
  void connect({required VoidCallback onConnect});
  Future<dynamic> sendToken({
    required String receiverAddress,
    required double amount,
    required String memo,
    required String networkCode,
    required String assetCode,
  });
  Future<void> changeNetwork(SupportedNetwork network);
  void disconnect();
  ReownAppKitModalWalletInfo? get selectedWallet;
}

@Singleton(as: IReownService)
final class ReownService implements IReownService {
  late ReownAppKitModal _appKitModal;
  bool isInitialized = false;
  VoidCallback? _onConnect;
  final _streamController = StreamController<DateTime>.broadcast();

  @override
  Stream<DateTime> get onChanged => _streamController.stream;
  @override
  bool get isConnected => _appKitModal.isConnected;
  @override
  ReownAppKitModalWalletInfo? get selectedWallet => _appKitModal.selectedWallet;

  @override
  void connect({required VoidCallback onConnect}) {
    _onConnect = onConnect;
    _appKitModal.openModalView();
  }

  @override
  void disconnect() async {
    await _appKitModal.disconnect();
  }

  void notifyListeners(String reason) {
    print('IReownService:notifyListeners: $reason');
    _streamController.add(DateTime.now());
  }

  @override
  void init({required BuildContext context}) async {
    if (isInitialized) return;

    if (kDebugMode) {
      ReownAppKitModalNetworks.addSupportedNetworks(
        NetworkUtils.eip155,
        ReownConstants.supportedNetworks,
      );
    }

    _appKitModal = ReownAppKitModal(
      context: context,
      projectId: ReownConstants.projectId,
      enableAnalytics: true,
      logLevel: LogLevel.debug,
      includedWalletIds: {
        ReownConstants.trustWalletId,
        ReownConstants.metaMaskWalletId,
      },
      metadata: const PairingMetadata(
        name: '{AppName}',
        description: '{Description}',
        url: ReownConstants.xboUrl,
        icons: [ReownConstants.xboIconUrl],
        redirect: Redirect(native: ReownConstants.xboRedirectUrl),
      ),
    );

    // Set up listeners for modal events
    _appKitModal.onModalConnect.subscribe((ModalConnect? event) {
      notifyListeners('onModalConnect');
      _onConnect?.call();
    });
    _appKitModal.onModalUpdate
        .subscribe((ModalConnect? event) => notifyListeners('onModalUpdate'));
    _appKitModal.onModalNetworkChange.subscribe(
        (ModalNetworkChange? event) => notifyListeners('onModalNetworkChange'));
    _appKitModal.onModalDisconnect.subscribe(
        (ModalDisconnect? event) => notifyListeners('onModalDisconnect'));
    _appKitModal.onModalError
        .subscribe((ModalError? event) => notifyListeners('onModalError'));
    _appKitModal.onSessionExpireEvent.subscribe(
        (SessionExpire? event) => notifyListeners('onSessionExpireEvent'));
    _appKitModal.onSessionUpdateEvent.subscribe(
        (SessionUpdate? event) => notifyListeners('onSessionUpdateEvent'));
    _appKitModal.onSessionEventEvent.subscribe(
        (SessionEvent? event) => notifyListeners('onSessionEventEvent'));
    _appKitModal.appKit!.core.relayClient.onRelayClientConnect.subscribe(
        (EventArgs? event) => notifyListeners('onRelayClientConnect'));
    _appKitModal.appKit!.core.relayClient.onRelayClientError
        .subscribe((EventArgs? event) => notifyListeners('onRelayClientError'));
    _appKitModal.appKit!.core.relayClient.onRelayClientDisconnect.subscribe(
        (EventArgs? event) => notifyListeners('onRelayClientDisconnect'));

    try {
      await _appKitModal.init();
      isInitialized = true;
      notifyListeners('isInitialized');
    } catch (e) {
      print('IReownService:error: $e');
      notifyListeners('isInitialized');
    }
  }

  @override
  Future<void> changeNetwork(SupportedNetwork network) async {
    if (!isConnected) return;

    try {
      if (_appKitModal.selectedChain?.chainId == network.chainID) return;
      final chain = ReownAppKitModalNetworks.getNetworkById(
          NetworkUtils.eip155, network.chainID ?? '');
      await _appKitModal.selectChain(chain, switchChain: true);
      _appKitModal.launchConnectedWallet();
      final chainId = int.tryParse(network.chainID ?? '0');
      return await _appKitModal.request(
        topic: _appKitModal.session!.topic,
        chainId: _appKitModal.selectedChain!.chainId,
        request: SessionRequestParams(
          method: MethodsConstants.walletSwitchEthChain,
          params: [
            {'chainId': '0x${chainId?.toRadixString(16)}'}
          ],
        ),
      );
    } catch (e) {
      _streamController.addError(e);
      print('IReownService:error: $e');
    }
  }

  @override
  Future sendToken({
    required String receiverAddress,
    required double amount,
    required String memo,
    required String networkCode,
    required String assetCode,
  }) async {
    final network = SupportedNetwork.getNetworkFor(
        assetCode: assetCode, networkCode: networkCode);
    if (network == null) return;

    try {
      if (network.contractAddress.isNotEmpty) {
        return _sendOtherTokens(
            receiverAddress: receiverAddress, amount: amount, network: network);
      }

      final EthereumAddress receiver = EthereumAddress.fromHex(receiverAddress);
      final BigInt weiValue = BigInt.from(amount * 1e18);
      final address = _appKitModal.session!.getAddress(NetworkUtils.eip155);
      if (address == null) return;

      final sender = EthereumAddress.fromHex(address);
      final transaction = Transaction(
          from: sender, to: receiver, value: EtherAmount.inWei(weiValue));
      _appKitModal.launchConnectedWallet();

      return await _appKitModal
          .request(
        topic: _appKitModal.session!.topic,
        chainId: _appKitModal.selectedChain!.chainId,
        request: SessionRequestParams(
          method: MethodsConstants.ethSendTransaction,
          params: [transaction.toJson()],
        ),
      )
          .catchError((error) {
        print('IReownService:error: $error');
        _streamController.addError(error);
      });
    } catch (e) {
      _streamController.addError(e);
      print('IReownService:error: $e');
    }
  }

  Future<dynamic> _sendOtherTokens({
    required String receiverAddress,
    required double amount,
    required SupportedNetwork network,
  }) async {
    try {
      final EthereumAddress receiver = EthereumAddress.fromHex(receiverAddress);
      final int decimals = network.decimals;
      final BigInt weiValue =
          BigInt.from(amount * BigInt.from(10).pow(decimals).toInt());
      final tokenContractAddress =
          EthereumAddress.fromHex(network.contractAddress);
      final address = _appKitModal.session!.getAddress(NetworkUtils.eip155);
      if (address == null) {
        print('Error: No sender address found.');
        return;
      }
      final sender = EthereumAddress.fromHex(address);
      const bep20Abi = '''
      [
        {
          "constant": false,
          "inputs": [
            { "name": "_to", "type": "address" },
            { "name": "_value", "type": "uint256" }
          ],
          "name": "transfer",
          "outputs": [{ "name": "", "type": "bool" }],
          "type": "function"
        }
      ]
      ''';

      final contract = DeployedContract(
          ContractAbi.fromJson(bep20Abi, network.id), tokenContractAddress);
      final transaction = Transaction.callContract(
        from: sender,
        contract: contract,
        function: contract.function('transfer'),
        parameters: [receiver, weiValue],
      );
      _appKitModal.launchConnectedWallet();

      return await _appKitModal
          .request(
        topic: _appKitModal.session!.topic,
        chainId: network.chainID?.toString() ?? '',
        request: SessionRequestParams(
          method: MethodsConstants.ethSendTransaction,
          params: [transaction.toJson()],
        ),
      )
          .catchError((error) {
        print('IReownService:error: $error');
        _streamController.addError(error);
      });
    } catch (e) {
      print('IReownService:error: $e');
      _streamController.addError(e);
    }
  }
}

Please if you could create a simple project that would help me a lot indeed.

Was it fixed @damienissa? Would be nice to have a description of the problem so it could help others.

Yeah, the problem was with this part of code:

if (kDebugMode) {
      ReownAppKitModalNetworks.addSupportedNetworks(
        NetworkUtils.eip155,
        ReownConstants.supportedNetworks,
      );
    }

When I run it with release mode, Testnet networks is no longer available and an exception is received.

@quetool This is my service for working with the appKit. Also, I can create a simple project with this service and place it on github repo.

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:injectable/injectable.dart';
import 'package:reown_appkit/reown_appkit.dart' hide ReownConstants;

import 'networks.dart';
import 'reown_constants.dart';

abstract interface class IReownService {
  bool get isConnected;
  void init({required BuildContext context});
  Stream<DateTime> get onChanged;
  void connect({required VoidCallback onConnect});
  Future<dynamic> sendToken({
    required String receiverAddress,
    required double amount,
    required String memo,
    required String networkCode,
    required String assetCode,
  });
  Future<void> changeNetwork(SupportedNetwork network);
  void disconnect();
  ReownAppKitModalWalletInfo? get selectedWallet;
}

Thanks for sharing the code!

Can I ask one question? Is it OK to add such getter with hardcoded namespace string?

String? get currentAddress => _appKitModal.session?.getAddress('eip155');

It stands for Ethereum, but anyway, can that value change over time..? And how to handle user selection of something non-EVM? 🤔

@vitalii-petrun You can get the current namespace based on chainId and then use this value in getAddress()

final chainId = '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; // Solana mainnet
final namespace = ReownAppKitModalNetworks.getNamespaceForChainId(chainId); // `solana` namespace
final address = _appKitModal.session?.getAddress(namespace); // Solana address