dapp_injected_eip155 1.0.4 copy "dapp_injected_eip155: ^1.0.4" to clipboard
dapp_injected_eip155: ^1.0.4 copied to clipboard

Support to inject the provider API into websites visited by its users using the window.ethereum provider object

example/lib/main.dart

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
  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
  State<MyHomePage> createState() => _MyHomePageState();
}

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

  @override
  void dispose() {
    _urlController.dispose();
    super.dispose();
  }

  @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
  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
  void dispose() {
    _progressController.sink.close();
    _progressController.close();
    super.dispose();
  }

  @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;
      ''',

      /// set isMetaMask flag
      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,

        /// warning if set 'true'
      ),
      methodCallbacks: MethodCallbacks(
        onSignTransaction: _onSignTransaction,
        onSwitchNetwork: (chainId) async {
          /// need check case chainId not support
          int index = networkLi.indexWhere((e) => e.chainId == chainId);
          if (index == -1) {
            throw const NotFoundChainException();
          }
          // currentNetwork = networkLi[index];
          return networkLi[index].rpcUrl;
        },
        onAddNetwork: (data) async {
          ///user reject
          // 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('Cancel'),
                    ),
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () async {
                        Navigator.of(context).pop(true);
                      },
                      child: const Text('Confirm'),
                    ),
                  ),
                ],
              )
            ],
          ),
        );
      },
    );
    return status ?? false;
  }

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

    /// if user reject
    // 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) {
      // user reject
      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) {
      // user reject
      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) {
      // user reject
      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;
  }
}
0
likes
160
points
18
downloads

Publisher

unverified uploader

Weekly Downloads

Support to inject the provider API into websites visited by its users using the window.ethereum provider object

Homepage
Repository (GitHub)

Documentation

API reference

License

BSD-2-Clause (license)

Dependencies

flutter, flutter_inappwebview

More

Packages that depend on dapp_injected_eip155