Flutter区块链交互插件dapp_injected_eip155的使用

Flutter区块链交互插件dapp_injected_eip155的使用

dapp_injected_eip155

支持在以太坊网络上注入的DApp - EIP155

特性

支持以太坊网络

方法 支持
signTransaction ✔️
signMessage ✔️
signTypedMessage ✔️
signPersonalMessage ✔️
sendTransaction ✔️
ecRecover
requestAccounts ✔️
watchAsset
addEthereumChain ✔️
switchEthereumChain ✔️

支持

有两项功能我还没有测试过:

  • watchAsset
  • ecRecover

请提供我关于如何测试watchAssetecRecover的信息和步骤。

我的地址: [0xDBEC6913a697B61F218B4a5D33B7561800Fe04E9]

安装

首先,在你的pubspec.yaml文件中添加dapp_injected_eip155作为依赖项。

dependencies:
  dapp_injected_eip155: ^1.0.4

要求

首次运行此函数以确保库正常工作。

await ScriptUtils.initProviderScript();

方法

wallet_addEthereumChain

创建一个确认请求,让用户将指定链添加到钱包应用。调用者必须指定链ID和一些链元数据。钱包应用可能会拒绝或接受请求。如果链被添加,则返回null,否则返回错误。由EIP-3085引入。

wallet_switchEthereumChain

请求钱包切换其活动的以太坊链。由EIP-3326引入。

Future<NetworkSupport> _switchChain(int chainId) async {
  /// 1. 检查情况:未知的链ID: \(chainId)
  ///
  int index = networkLi.indexWhere((e) => e.chainId == chainId);
  if (index == -1) {
    throw const NotFoundChainException();
  }
  /// 2. 显示确认切换链的对话框
  /// 用户拒绝
  if (userReject) {
    throw const UserRejectException();
    // 或者使用RpcException带有errorCode和message
    // throw const RpcException(4001, 'User rejected the request.');
  }
  return networkLi[index].rpcUrl;
}

personal_sign

向用户展示纯文本签名挑战,并返回签名响应。在某些其他钱包上等同于eth_sign,并为签名的消息添加安全前缀,以防止挑战欺骗用户签署金融交易。此方法要求用户已授权与他们的帐户进行交互,因此请确保先调用eth_requestAccounts(推荐)或wallet_requestPermissions

eth_signTypedData_v4

向用户展示结构化且可读的数据消息以供签署,并返回签名响应。由EIP-712引入。此方法要求用户已授权与他们的帐户进行交互,因此请确保先调用eth_requestAccounts(推荐)或wallet_requestPermissions

eth_sendTransaction

创建一个新的钱包确认,从用户的帐户发起以太坊交易。此方法要求用户已授权与他们的帐户进行交互,因此请确保先调用eth_requestAccounts(推荐)或wallet_requestPermissions

Future<String> _onSignTransaction(int? chainId, String? rpcUrl, Map<String, dynamic> data) async {
  if (chainId == null || rpcUrl == null) {
    throw const NotFoundChainException();
  }
  /// 1. 未经授权
  /// 请求的帐户和/或方法未得到用户的授权。
  /// throw const UnauthorizedException();
  /// 
  ///  2. 用户拒绝
  /// throw const RpcException(4001, 'User rejected the request.');
  /// 
  JsTransaction transaction = JsTransaction.fromJson(data);
  try {
    final tx = Transaction(
      from: EthereumAddress.fromHex(transaction.from),
      data: hexToBytes(transaction.data),
      value: EtherAmount.fromBase10String(EtherUnit.wei, transaction.value ?? '0'),
      to: EthereumAddress.fromHex(transaction.to),
      // maxGas: 61931, /// mock or transaction.gas
    );
    final client = Web3Client(rpcUrl, http.Client());
    final hash = await client.sendTransaction(
      EthPrivateKey.fromHex(_privateKey),
      tx,
      chainId: chainId,
    );
    return hash;
  } on RPCError catch(e) {
    throw RpcException(e.errorCode, e.message);
  } catch(e) {
    rethrow;
  }
}

错误

要发送错误消息,需要抛出异常。我已经提供了以下特定异常:

  • throw UserRejectException();
  • throw NotFoundChainException();
  • throw InvalidInputException();
  • throw UnSupportMethodException();
  • throw UnauthorizedException();

此外,您可以编辑异常以包含特定的errorCodemessage

throw RpcException(4001, 'User rejected the request.');

/// errorCode: 4001
/// message: User rejected the request.

提供商错误

代码 消息
4001 用户拒绝了请求。
4100 请求的帐户和/或方法未得到用户的授权。
4200 请求的方法不被此以太坊提供商支持。
4900 提供商已断开与所有链的连接。
4901 提供商已断开与指定链的连接。

RPC 错误

代码 消息
-32000 输入无效。
-32001 资源未找到。
-32002 资源不可用。
-32003 交易被拒绝。
-32004 方法不受支持。
-32005 请求限制超出。
-32700 服务器收到无效的JSON。解析JSON时服务器端发生错误。
-32600 发送的JSON不是有效的请求对象。
-32601 方法不存在/不可用。
-32602 无效的方法参数。
-32603 JSON-RPC内部错误。
4902 无法识别的链ID。尝试先使用wallet_addEthereumChain添加链。

示例代码

import 'dart:async';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart';
import 'package:web3dart/crypto.dart';
import 'package:dapp_injected_eip155/dapp_injected_eip155.dart';
import 'package:web3dart/json_rpc.dart';
import 'js_transaction.dart';
import 'package:web3dart/web3dart.dart';
import 'package:http/http.dart' as http;
import 'network_support.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'widgets/progress_painter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await ScriptUtils.initProviderScript();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        appBarTheme: const AppBarTheme(backgroundColor: Colors.white),
        scaffoldBackgroundColor: Colors.white,
        brightness: Brightness.light,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final TextEditingController _urlController =
      TextEditingController(text: 'https://pancakeswap.finance/');

  [@override](/user/override)
  void dispose() {
    _urlController.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _urlController,
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: () async {
                Navigator.of(context).push(
                  MaterialPageRoute(
                      builder: (c) => ExampleDapp(
                            initialUrl: _urlController.text.trim(),
                          )),
                );
              },
              child: const Text('DApp Injected'),
            ),
          ],
        ),
      ),
    );
  }
}

class ExampleDapp extends StatefulWidget {
  final String initialUrl;
  const ExampleDapp({super.key, required this.initialUrl});

  [@override](/user/override)
  State<ExampleDapp> createState() => _ExampleDappState();
}

class _ExampleDappState extends State<ExampleDapp> {
  final List<NetworkSupport> networkLi = [
    NetworkSupport(
      chainId: 1,
      chainName: 'Ethereum',
      nativeCurrency: NativeCurrency(
        name: 'ETH',
        symbol: 'eth',
        decimals: 18,
      ),
      rpcUrls: [
        "https://rpc.ankr.com/eth",
      ],
      blockExplorerUrls: [
        "https://etherscan.io/tx/",
      ],
    ),
  ];
  final List<String> addressLi = [
    '0xDBEC6913a697B61F218B4a5D33B7561800Fe04E9',
    '0x1bc9BDF4f77AD6662adD75628c6A65B4062Fd3f3',
  ];
  final String _privateKey = ''; // mock
  late NetworkSupport initNetwork = networkLi.first;
  late String currentAddress = addressLi.first;

  final StreamController<double> _progressController =
      StreamController<double>();
  InAppWebViewController? _webViewController;

  [@override](/user/override)
  void dispose() {
    _progressController.sink.close();
    _progressController.close();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(),
      body: SafeArea(
        top: false,
        child: _buildBody(context),
      ),
    );
  }

  AppBar _buildAppBar() {
    return AppBar(
      leading: IconButton(
        icon: const Icon(Icons.arrow_back_ios_new_rounded),
        onPressed: () async {
          bool canBack = (await _webViewController?.canGoBack()) ?? false;
          if (canBack) {
            await _webViewController?.goBack();
            return;
          }
          if (mounted) {
            Navigator.of(context).pop();
          }
        },
      ),
      title: const Text('DApp Browser'),
      bottom: PreferredSize(
        preferredSize: const Size.fromHeight(2),
        child: StreamBuilder<double>(
          initialData: 0,
          stream: _progressController.stream,
          builder: (c, asyncSnapshot) {
            if (asyncSnapshot.data == 1) {
              return const SizedBox();
            }
            return ProgressPainter(
              progress: asyncSnapshot.data!,
              baseColor: Theme.of(context).appBarTheme.backgroundColor,
              primaryColor: Colors.blueAccent,
            );
          },
        ),
      ),
      actions: [
        _buildAccountSwitcher(context),
      ],
    );
  }

  Widget _buildBody(BuildContext context) {
    return DAppInjectedView(
      loadingChild: const Center(
        child: SizedBox(
          height: 32,
          width: 32,
          child: CircularProgressIndicator(),
        ),
      ),
      initialScript: '''
          window.ethereum.isMetaMask = true;
      ''',

      /// 设置isMetaMask标志
      initialUrl: widget.initialUrl,
      currentProvider: WalletWeb3Provider(
        address: currentAddress,
        chainId: initNetwork.chainId,
        rpcUrl: initNetwork.rpcUrl,
      ),
      initialSettings: InAppWebViewSettings(
        isInspectable: true, // kDebugMode,
        mediaPlaybackRequiresUserGesture: true,
        allowsInlineMediaPlayback: true,
        iframeAllowFullscreen: true,
        javaScriptEnabled: true,
        supportMultipleWindows: false,

        /// 警告:如果设置为'true'
      ),
      methodCallbacks: MethodCallbacks(
        onSignTransaction: _onSignTransaction,
        onSwitchNetwork: (chainId) async {
          /// 需要检查链ID是否不支持
          int index = networkLi.indexWhere((e) => e.chainId == chainId);
          if (index == -1) {
            throw const NotFoundChainException();
          }
          // currentNetwork = networkLi[index];
          return networkLi[index].rpcUrl;
        },
        onAddNetwork: (data) async {
          /// 用户拒绝
          // throw const UserRejectException();
          NetworkSupport network = NetworkSupport.fromJson(data);
          networkLi.add(network);

          return (chainId: network.chainId!, rpcUrl: network.rpcUrl);
        },
        onSignMessage: _onSignMessage,
        onSignPersonalMessage: _onSignPersonalMessage,
        onSignTypeMessage: _onSignTypeMessage,
        // onEcRecover: _onEcRecover,
      ),
      onWebViewCreated: (controller) {
        _webViewController = controller;
      },
      onWebViewClosed: () {
        _webViewController = null;
      },
      shouldOverrideUrlLoading: (c, navAction) async {
        // final url = navAction.request.url.toString();
        return NavigationActionPolicy.ALLOW;
      },
      onProgressChanged: (controller, progress) {
        _progressController.sink.add(progress / 100);
      },
      onReceivedError: (controller, request, error) {
        _progressController.sink.add(1);
      },
    );
  }

  Widget _buildAccountSwitcher(BuildContext context) {
    return IconButton(
      onPressed: () async {
        await showModalBottomSheet(
            context: context,
            builder: (context) {
              return ListView.builder(
                padding: const EdgeInsets.only(top: 25, bottom: 35),
                itemBuilder: (c, index) {
                  bool isActive = currentAddress == addressLi[index];
                  return GestureDetector(
                    onTap: () {
                      Navigator.of(context).pop();
                      if (isActive) return;
                      currentAddress = addressLi[index];
                      setState(() {});
                    },
                    child: Padding(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 12, vertical: 16),
                      child: Text(
                        addressLi[index],
                        style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                              color: isActive
                                  ? Theme.of(context).primaryColor
                                  : null,
                            ),
                      ),
                    ),
                  );
                },
                itemCount: addressLi.length,
              );
            });
      },
      icon: const Icon(Icons.menu),
    );
  }

  Future<bool> _confirmSignMessage(String title, String message) async {
    bool? status = await showModalBottomSheet(
      context: context,
      builder: (c) {
        return Padding(
          padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 16),
          child: Column(
            children: [
              Text(
                title,
                style: Theme.of(context).textTheme.headlineLarge,
              ),
              const SizedBox(height: 25),
              Expanded(
                child: SingleChildScrollView(
                  child: Text(
                    message,
                    style: Theme.of(context).textTheme.bodyLarge,
                  ),
                ),
              ),
              const SizedBox(height: 16),
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () async {
                        Navigator.of(context).pop(false);
                      },
                      child: const Text('取消'),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () async {
                        Navigator.of(context).pop(true);
                      },
                      child: const Text('确认'),
                    ),
                  ),
                ],
              )
            ],
          ),
        );
      },
    );
    return status ?? false;
  }

  Future<String> _onSignTransaction(
      int? chainId, String? rpcUrl, Map<String, dynamic> data) async {
    if (chainId == null || rpcUrl == null) {
      throw const NotFoundChainException();
    }

    /// 如果用户拒绝
    // throw const UserRejectException();
    JsTransaction transaction = JsTransaction.fromJson(data);
    try {
      final tx = Transaction(
        from: EthereumAddress.fromHex(transaction.from),
        data: hexToBytes(transaction.data),
        value: EtherAmount.fromBase10String(
            EtherUnit.wei, transaction.value ?? '0'),
        to: EthereumAddress.fromHex(transaction.to),
        maxGas: 61931,

        /// mock or transaction.gas
      );
      final client = Web3Client(rpcUrl, http.Client());
      final hash = await client.sendTransaction(
        EthPrivateKey.fromHex(_privateKey),
        tx,
        chainId: chainId,
      );
      return hash;
    } on RPCError catch (e) {
      throw RpcException(e.errorCode, e.message);
    } catch (e) {
      rethrow;
    }
  }

  Future<String> _onSignMessage(String type, String message) async {
    bool isAccept = await _confirmSignMessage(type, message);
    if (!isAccept) {
      // 用户拒绝
      throw const UserRejectException();
    }
    String signature = EthSigUtil.signMessage(
      privateKey: _privateKey,
      message: hexToBytes(message),
    );
    return signature;
  }

  Future<String> _onSignPersonalMessage(String type, String message) async {
    bool isAccept = await _confirmSignMessage(type, message);
    if (!isAccept) {
      // 用户拒绝
      throw const UserRejectException();
    }
    String signature = EthSigUtil.signPersonalMessage(
      privateKey: _privateKey,
      message: hexToBytes(message),
    );
    return signature;
  }

  Future<String> _onSignTypeMessage(String type, JsSignTypeData data) async {
    bool isAccept = await _confirmSignMessage(type, data.raw);
    if (!isAccept) {
      // 用户拒绝
      throw const UserRejectException();
    }
    TypedDataVersion? version;
    switch (data.version) {
      case 'V1':
        version = TypedDataVersion.V1;
        break;
      case 'V3':
        version = TypedDataVersion.V3;
        break;
      case 'V4':
        version = TypedDataVersion.V4;
        break;
      default:
        break;
    }
    if (version == null) {
      throw const InvalidInputException();
    }
    final String signature = EthSigUtil.signTypedData(
      privateKey: _privateKey,
      jsonData: data.raw,
      version: version,
    );
    return signature;
  }
}

更多关于Flutter区块链交互插件dapp_injected_eip155的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter区块链交互插件dapp_injected_eip155的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何使用Flutter区块链交互插件dapp_injected_eip155的示例代码。这个插件允许你与遵循EIP-155标准的Web3钱包(如MetaMask)进行交互。假设你已经设置好了Flutter开发环境,并且已经添加了dapp_injected_eip155依赖到你的pubspec.yaml文件中。

1. 添加依赖

首先,确保在pubspec.yaml中添加dapp_injected_eip155依赖:

dependencies:
  flutter:
    sdk: flutter
  dapp_injected_eip155: ^latest_version  # 替换为最新版本号

2. 导入包并初始化

在你的Dart文件中导入dapp_injected_eip155包,并初始化:

import 'package:flutter/material.dart';
import 'package:dapp_injected_eip155/dapp_injected_eip155.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Ethereum? ethereumClient;

  @override
  void initState() {
    super.initState();
    initEthereumClient();
  }

  Future<void> initEthereumClient() async {
    try {
      ethereumClient = await Ethereum.connectEIP155();
      print('Connected to Ethereum client: ${ethereumClient!.clientVersion}');
    } catch (e) {
      print('Failed to connect to Ethereum client: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter EIP-155 Interaction'),
        ),
        body: Center(
          child: ethereumClient != null
              ? Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text('Account: ${ethereumClient!.selectedAddress!}'),
                    ElevatedButton(
                      onPressed: () async {
                        await fetchBalance();
                      },
                      child: Text('Fetch Balance'),
                    ),
                  ],
                )
              : CircularProgressIndicator(),
        ),
      ),
    );
  }

  Future<void> fetchBalance() async {
    if (ethereumClient == null) return;

    try {
      BigInt balance = await ethereumClient!.getBalance(ethereumClient!.selectedAddress!);
      print('Balance: $balance wei');
      // Convert wei to ether if needed
      double etherBalance = balance.toDouble() / (10 ** 18);
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Balance: $etherBalance ether')),
      );
    } catch (e) {
      print('Failed to fetch balance: $e');
    }
  }
}

3. 解释代码

  • 依赖导入:在pubspec.yaml中添加dapp_injected_eip155依赖。
  • 初始化Ethereum客户端:在initState方法中,调用Ethereum.connectEIP155()来连接到用户的Ethereum客户端(如MetaMask)。
  • UI显示:如果成功连接到Ethereum客户端,显示用户的账户地址,并提供一个按钮来获取余额。
  • 获取余额:点击按钮时,调用ethereumClient!.getBalance()方法来获取用户的余额,并将其从wei单位转换为ether单位显示。

注意事项

  • 确保你的应用运行在支持Web3注入的钱包环境中,如MetaMask浏览器扩展。
  • 处理错误和异常,尤其是在处理区块链交互时,网络延迟和节点问题可能会导致请求失败。
  • 考虑到安全性,确保你的应用不会泄露用户的私钥或敏感信息。

这个示例展示了如何使用dapp_injected_eip155插件与Ethereum客户端进行基本的交互。根据需求,你可以扩展这个插件的功能,如发送交易、读取智能合约数据等。

回到顶部