diff --git a/CHANGELOG.md b/CHANGELOG.md index e29420f..c7b22b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0-pre.7 + +- Pre-release for utils + ## 2.1.9 - Add `toList` for list case of argument (#38) @@ -23,7 +27,7 @@ ## 2.1.3 -- Add provider call error catching +- Add provider call error catching - Fix ethereum error not thrown properly - Add documentation for getFeeData diff --git a/README.md b/README.md index 5224441..f6e00fe 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,19 @@ To use Ethers JS and Wallet Connect Provider, we need to include script to JS pa ```html - + - + +``` + +Optinally, use injector by asynchronous calling `inject` or `injectAll` before `runApp`. + +```dart +void main() async { + await FlutterWeb3.injectAll(); + + runApp(MyApp()); +} ``` --- diff --git a/example/pubspec.lock b/example/pubspec.lock index ebe012c..14eb088 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,13 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + amdjs: + dependency: transitive + description: + name: amdjs + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +42,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -50,6 +64,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + dom_tools: + dependency: transitive + description: + name: dom_tools + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + enum_to_string: + dependency: transitive + description: + name: enum_to_string + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" fake_async: dependency: transitive description: @@ -73,7 +101,7 @@ packages: path: ".." relative: true source: path - version: "2.0.0-pre.6" + version: "2.2.0-pre.5" get: dependency: "direct main" description: @@ -81,6 +109,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.1.4" + html_unescape: + dependency: transitive + description: + name: html_unescape + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" js: dependency: transitive description: @@ -88,6 +130,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" + json_object_mapper: + dependency: transitive + description: + name: json_object_mapper + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" matcher: dependency: transitive description: @@ -101,7 +157,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -109,6 +165,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + resource_portable: + dependency: transitive + description: + name: resource_portable + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -142,6 +205,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + swiss_knife: + dependency: transitive + description: + name: swiss_knife + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.8" term_glyph: dependency: transitive description: @@ -155,7 +225,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: diff --git a/lib/ethers.dart b/lib/ethers.dart index 0549e18..f99ba3c 100644 --- a/lib/ethers.dart +++ b/lib/ethers.dart @@ -1,4 +1,3 @@ export 'src/ethers/constant.dart'; export 'src/ethers/ethers.dart'; export 'src/ethers/exception.dart'; -export 'src/ethers/utils.dart'; diff --git a/lib/flutter_web3.dart b/lib/flutter_web3.dart index 921c3ab..aed1494 100644 --- a/lib/flutter_web3.dart +++ b/lib/flutter_web3.dart @@ -1,4 +1,77 @@ +import 'package:amdjs/amdjs.dart'; + export './ethereum.dart'; export './ethers.dart'; export './src/constant.dart'; +export './utils.dart'; export './wallet_connect.dart'; + +/// Static class for injecting required js module. +class FlutterWeb3 { + /// Inject js module that required by this package by [injectionType]. Optinally [version] can be provided, otherwise `latest` is used. + /// + /// --- + /// + /// ```dart + /// void main() async { + /// await FlutterWeb3.inject(FlutterWeb3InjectionTypes.ethers); + /// + /// runApp(MyApp()); + /// } + /// ``` + static Future inject(FlutterWeb3InjectionTypes injectionType, + [String version = 'latest']) async { + AMDJS.verbose = false; + + await AMDJS.require( + injectionType.module, + jsFullPath: injectionType.path.replaceFirst(r'latest', version), + globalJSVariableName: injectionType.variable, + ); + } + + /// Inject all js module that required by this package at `latest` version. + /// + /// --- + /// + /// ```dart + /// void main() async { + /// await FlutterWeb3.injectAll(); + /// + /// runApp(MyApp()); + /// } + /// ``` + static Future injectAll() async { + AMDJS.verbose = false; + await Future.wait(FlutterWeb3InjectionTypes.values.map((e) => inject(e))); + } +} + +/// Available module to inject, used in [FlutterWeb3.inject]. +enum FlutterWeb3InjectionTypes { + ethers, + walletConnect, +} + +extension _InjectionInformation on FlutterWeb3InjectionTypes { + static const _info = { + FlutterWeb3InjectionTypes.ethers: { + 'module': 'ethers', + 'variable': 'ethers', + 'path': + 'https://cdn.jsdelivr.net/npm/ethers@latest/dist/ethers.umd.min.js' + }, + FlutterWeb3InjectionTypes.walletConnect: { + 'module': 'WalletConnectProvider', + 'variable': 'WalletConnectProvider', + 'path': + 'https://cdn.jsdelivr.net/npm/@walletconnect/web3-provider@latest/dist/umd/index.min.js' + } + }; + + String get module => _info[this]!['module']!; + + String get variable => _info[this]!['variable']!; + + String get path => _info[this]!['path']!; +} diff --git a/lib/src/ethereum/exception.dart b/lib/src/ethereum/exception.dart index e5d8e3c..67e0750 100644 --- a/lib/src/ethereum/exception.dart +++ b/lib/src/ethereum/exception.dart @@ -1,25 +1,30 @@ -class EthereumUnrecognizedChainException implements Exception { - final int chainId; +class EthereumException implements Exception { + final int code; + final String message; + final dynamic data; - EthereumUnrecognizedChainException(this.chainId); + const EthereumException(this.code, this.message, this.data); @override - String toString() => - 'EthereumUnrecognizedChainException: Chain $chainId is not recognized, please add the chain using `walletAddChain` first'; + String toString() => 'EthereumException: $code $message'; } -class EthereumUserRejected implements Exception { +class EthereumUnrecognizedChainException extends EthereumException { + final int chainId; + + const EthereumUnrecognizedChainException(this.chainId, + [int code = 4902, String message = '']) + : super(code, message, null); + @override - String toString() => 'EthereumUserRejected: User rejected the request'; + String toString() => + 'EthereumUnrecognizedChainException: Chain $chainId is not recognized, please add the chain using `walletAddChain` first'; } -class EthereumException implements Exception { - final int code; - final String message; - final dynamic data; - - EthereumException(this.code, this.message, this.data); +class EthereumUserRejected extends EthereumException { + const EthereumUserRejected([int code = 4001, String message = '']) + : super(code, message, null); @override - String toString() => 'EthereumException: $code $message'; + String toString() => 'EthereumUserRejected: User rejected the request'; } diff --git a/lib/src/ethers/contract.dart b/lib/src/ethers/contract.dart index 5339299..485ed70 100644 --- a/lib/src/ethers/contract.dart +++ b/lib/src/ethers/contract.dart @@ -72,6 +72,15 @@ class Contract extends Interop<_ContractImpl> { Future call(String method, [List args = const []]) => _call(method, args); + /// Returns a new instance of the [Contract] attached to [addressOrName]. + /// + /// This is useful if there are multiple similar or identical copies of a Contract on the network and you wish to interact with each of them. + Contract attach(String addressOrName) { + assert(EthUtils.isAddress(addressOrName), 'addressOrName must be valid'); + + return Contract._(impl.attach(addressOrName)); + } + ///Returns a new instance of the [Contract], but connected to [Provider] or [Signer]. /// ///By passing in a [Provider], this will return a downgraded Contract which only has read-only access (i.e. constant calls). @@ -115,16 +124,45 @@ class Contract extends Interop<_ContractImpl> { List listeners(Object event) => impl.listeners(event is EventFilter ? event.impl : event); - /// Multicall read-only constant [method] with [args]. `May not` be at the same block. + /// Multicall read-only constant [method] with [args]. Will use multiple https call unless [multicall] is provided. /// /// If [eagerError] is `true`, returns the error immediately on the first error found. - Future> multicall(String method, List> args, - [bool eagerError = false]) => - Future.wait( - Iterable.generate(args.length).map( - (e) => _call(method, args[e]), - ), - eagerError: eagerError); + Future> multicall( + String method, + List> args, [ + Multicall? multicall, + bool eagerError = false, + ]) async { + if (multicall != null) { + final res = await multicall.aggregate( + args + .map( + (e) => MulticallPayload.fromInterfaceFunction( + address, interface, method, e), + ) + .toList(), + ); + final decoded = res.returnData + .map((e) => interface.decodeFunctionResult(method, e)) + .toList(); + switch (T) { + case List: + return decoded as List; + case BigInt: + return decoded.map((e) => BigInt.parse(e[0].toString())).toList() + as List; + default: + return decoded.map((e) => e[0]).toList() as List; + } + } else { + return Future.wait( + Iterable.generate(args.length).map( + (e) => _call(method, args[e]), + ), + eagerError: eagerError, + ); + } + } /// Remove a [listener] for the [event]. If no [listener] is provided, all listeners for [event] are removed. off(dynamic event, [Function? listener]) => callMethod( diff --git a/lib/src/ethers/ethers.dart b/lib/src/ethers/ethers.dart index 1555f73..b880617 100644 --- a/lib/src/ethers/ethers.dart +++ b/lib/src/ethers/ethers.dart @@ -9,6 +9,7 @@ import '../ethereum/ethereum.dart'; import '../ethereum/exception.dart'; import '../ethereum/utils.dart'; import '../interop_wrapper.dart'; +import '../utils/multicall.dart'; import '../wallet_connect/wallet_connect.dart'; part 'access_list.dart'; @@ -17,6 +18,7 @@ part 'contract.dart'; part 'event.dart'; part 'fee_data.dart'; part 'filter.dart'; +part 'fragment.dart'; part 'interface.dart'; part 'interop.dart'; part 'log.dart'; diff --git a/lib/src/ethers/fragment.dart b/lib/src/ethers/fragment.dart new file mode 100644 index 0000000..8264795 --- /dev/null +++ b/lib/src/ethers/fragment.dart @@ -0,0 +1,162 @@ +part of ethers; + +class ConstructorFragment + extends Fragment { + /// Creates a new [ConstructorFragment] from any compatible [source]. + factory ConstructorFragment.from(dynamic source) => ConstructorFragment._( + _ConstructorFragmentImpl.from(source is Interop ? source.impl : source) + as T, + ); + + const ConstructorFragment._(T impl) : super._(impl); + + /// This is the gas limit that should be used during deployment. It may be `null`. + BigInt? get gas => impl.gas?.toBigInt; + + /// This is whether the constructor may receive ether during deployment as an endowment (i.e. msg.value != 0). + bool get payable => impl.payable; + + /// This is the state mutability of the constructor. It can be any of: + /// - nonpayable + /// - payable + String get stateMutability => impl.stateMutability; +} + +class EventFragment extends Fragment<_EventFragmentImpl> { + /// Creates a new [EventFragment] from any compatible [source]. + factory EventFragment.from(dynamic source) => EventFragment._( + _EventFragmentImpl.from(source is Interop ? source.impl : source), + ); + + const EventFragment._(_EventFragmentImpl impl) : super._(impl); + + /// This is whether the event is anonymous. An anonymous Event does not inject its topic hash as topic0 when creating a log. + bool get anonymous => impl.anonymous; + + @override + String format([FormatTypes? type]) { + return toString(); + } + + @override + String toString() { + return 'EventFragment: $name anonymous: $anonymous'; + } +} + +/// An ABI is a collection of Fragments. +class Fragment extends Interop { + /// Creates a new [Fragment] sub-class from any compatible [source]. + factory Fragment.from(dynamic source) => Fragment._( + _FragmentImpl.from(source is Interop ? source.impl : source) as T); + + const Fragment._(T impl) : super.internal(impl); + + /// This is the name of the Event or Function. This will be `null` for a `ConstructorFragment`. + String? get name => impl.name; + + /// This is an array of each [ParamType] for the input parameters to the Constructor, Event of Function. + List get paramType => + impl.inputs.cast<_ParamTypeImpl>().map((e) => ParamType._(e)).toList(); + + /// This is a [String] which indicates the type of the [Fragment]. This will be one of: + /// - constructor + /// - event + /// - function + String get type => impl.type; + + /// Creates a [String] representation of the [Fragment] using the available [type] formats. + String format([FormatTypes? type]) => + type != null ? impl.format(type.impl) : impl.format(); + + @override + String toString() => 'Fragment: ${format()}'; +} + +class FunctionFragment extends ConstructorFragment<_FunctionFragmentImpl> { + /// Creates a new [FunctionFragment] from any compatible [source]. + factory FunctionFragment.from(dynamic source) => FunctionFragment._( + _FunctionFragmentImpl.from(source is Interop ? source.impl : source), + ); + + const FunctionFragment._(_FunctionFragmentImpl impl) : super._(impl); + + /// This is whether the function is constant (i.e. does not change state). This is `true` if the state mutability is `pure` or `view`. + bool get constant => impl.constant; + + /// A list of the Function output parameters. + List get outputs => + impl.outputs.cast<_ParamTypeImpl>().map((e) => ParamType._(e)).toList(); + + /// This is the state mutability of the constructor. It can be any of: + /// - nonpayable + /// - payable + /// - pure + /// - view + String get stateMutability => impl.stateMutability; + + @override + String format([FormatTypes? type]) { + return toString(); + } + + @override + String toString() { + return 'FunctionFragment: $name constant: $constant stateMutability: $stateMutability'; + } +} + +/// A representation of a solidity parameter. +class ParamType extends Interop<_ParamTypeImpl> { + factory ParamType.from(String source) => + ParamType._(_ParamTypeImpl.from(source)); + + const ParamType._(_ParamTypeImpl impl) : super.internal(impl); + + /// The type of children of the array. This is `null` for any parameter which is not an array. + ParamType? get arrayChildren => + impl.arrayChildren == null ? null : ParamType._(impl.arrayChildren!); + + /// The length of the array, or -1 for dynamic-length arrays. This is `null` for parameters which are not arrays. + int? get arrayLength => impl.arrayLength; + + /// The base type of the parameter. For primitive types (e.g. `address`, `uint256`, etc) this is equal to type. For arrays, it will be the string array and for a tuple, it will be the string tuple. + String get baseType => impl.baseType; + + ///The components of a tuple. This is `null` for non-tuple parameters. + List? get components => impl.components + ?.cast<_ParamTypeImpl>() + .map((e) => ParamType._(e)) + .toList(); + + /// Whether the parameter has been marked as indexed. This only applies to parameters which are part of an EventFragment. + bool get indexed => impl.indexed; + + /// The local parameter name. This may be null for unnamed parameters. For example, the parameter definition `string foobar` would be `foobar`. + String? get name => impl.name; + + /// The full type of the parameter, including tuple and array symbols. This may be `null` for unnamed parameters. For the above example, this would be `foobar`. + String? get type => impl.type; + + /// Creates a [String] representation of the [Fragment] using the available [type] formats. + String format([FormatTypes? type]) => + type != null ? impl.format(type.impl) : impl.format(); + + @override + String toString() => 'ParamType: ${format()}'; +} + +extension _FormatTypesExtImpl on FormatTypes { + dynamic get impl { + switch (this) { + case FormatTypes.json: + return _FormatTypesImpl.json; + case FormatTypes.minimal: + return _FormatTypesImpl.minimal; + case FormatTypes.full: + return _FormatTypesImpl.full; + case FormatTypes.sighash: + return _FormatTypesImpl.sighash; + } + } +} diff --git a/lib/src/ethers/interface.dart b/lib/src/ethers/interface.dart index 72fa499..4813368 100644 --- a/lib/src/ethers/interface.dart +++ b/lib/src/ethers/interface.dart @@ -6,15 +6,15 @@ enum FormatTypes { /// ''' /// [ /// { - /// "type": "function", - /// "name": "balanceOf", - /// "constant":true, - /// "stateMutability": "view", - /// "payable":false, "inputs": [ - /// { "type": "address", "name": "owner"} + /// 'type': 'function', + /// 'name': 'balanceOf', + /// 'constant':true, + /// 'stateMutability': 'view', + /// 'payable':false, 'inputs': [ + /// { 'type': 'address', 'name': 'owner'} /// ], - /// "outputs": [ - /// { "type": "uint256", "name": "balance"} + /// 'outputs': [ + /// { 'type': 'uint256', 'name': 'balance'} /// ] /// }, /// ] @@ -35,6 +35,9 @@ enum FormatTypes { /// ] /// ``` full, + + /// '0x70a08231' + sighash, } /// The Interface Class abstracts the encoding and decoding required to interact with contracts on the Ethereum network. @@ -53,15 +56,144 @@ class Interface extends Interop<_InterfaceImpl> { return Interface._(_InterfaceImpl(abi)); } - Interface._(_InterfaceImpl impl) : super.internal(impl); + const Interface._(_InterfaceImpl impl) : super.internal(impl); + + /// The [ConstructorFragment] for the interface. + ConstructorFragment get deploy => ConstructorFragment._(impl.deploy); + + /// All the [EventFragment] in the interface. + Map get events => + (dartify(impl.events) as Map) + .map((key, value) => MapEntry(key, EventFragment.from(jsify(value)))); + + /// All the [Fragment] in the interface. + List get fragments => + impl.fragments.cast<_FragmentImpl>().map((e) => Fragment._(e)).toList(); + + /// All the [FunctionFragment] in the interface. + Map get functions => (dartify(impl.functions) + as Map) + .map((key, value) => MapEntry(key, FunctionFragment.from(jsify(value)))); + + /// Returns the decoded values from the result of a call for [function] (see Specifying Fragments) for the given [data]. + /// + /// --- + /// + /// ```dart + /// // Decoding result data (e.g. from an eth_call) + /// resultData = '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + /// iface.decodeFunctionResult('balanceOf', resultData); + /// // [1000000000000000000] + /// ``` + List decodeFunctionResult(String function, String data) => + impl.decodeFunctionResult(function, data); + + /// Returns the decoded values from the result of a call for [function] (see Specifying Fragments) for the given [data]. + /// + /// --- + /// + /// ```dart + /// // Decoding result data (e.g. from an eth_call) + /// resultData = '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + /// iface.decodeFunctionResult(iface.fragments.first, resultData); + /// // [1000000000000000000] + /// ``` + List decodeFunctionResultFromFragment( + Fragment function, String data) => + impl.decodeFunctionResult(function.impl, data); + + /// Returns the encoded [topic] filter, which can be passed to getLogs for fragment (see Specifying Fragments) for the given [values]. + /// + /// Each topic is a 32 byte (64 nibble) `DataHexString`. + /// + /// --- + /// + /// ```dart + /// // Filter that matches all Transfer events + /// iface.encodeFilterTopics('Transfer', []); + /// // [ + /// // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' + /// // ] + /// + /// // Filter that matches the sender + /// iface.encodeFilterTopics('Transfer', [ + /// '0x8ba1f109551bD432803012645Ac136ddd64DBA72' + /// ]); + /// // [ + /// // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + /// // '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72' + /// // ] + /// ``` + List encodeFilterTopics(String topic, + [List values = const []]) => + impl.encodeFilterTopics(topic, values); + + /// Returns the encoded [topic] filter, which can be passed to getLogs for fragment (see Specifying Fragments) for the given [values]. + /// + /// Each topic is a 32 byte (64 nibble) `DataHexString`. + /// + /// --- + /// + /// ```dart + /// // Filter that matches all Transfer events + /// iface.encodeFilterTopics(iface.fragments.first, []); + /// // [ + /// // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' + /// // ] + /// + /// // Filter that matches the sender + /// iface.encodeFilterTopics(iface.fragments.first, [ + /// '0x8ba1f109551bD432803012645Ac136ddd64DBA72' + /// ]); + /// // [ + /// // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + /// // '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72' + /// // ] + /// ``` + List encodeFilterTopicsFromFragment(Fragment topic, + [List values = const []]) => + impl.encodeFilterTopics(topic.impl, values); + + /// Returns the encoded data, which can be used as the data for a transaction for [function] (see Specifying Fragments) for the given [values]. + /// + /// --- + /// + /// ```dart + /// // Encoding data for the tx.data of a call or transaction + /// iface.encodeFunctionData('transferFrom', [ + /// '0x8ba1f109551bD432803012645Ac136ddd64DBA72', + /// '0xaB7C8803962c0f2F5BBBe3FA8bf41cd82AA1923C', + /// '1' + /// ]); + /// // '0x23b872dd0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72000000000000000000000000ab7c8803962c0f2f5bbbe3fa8bf41cd82aa1923c0000000000000000000000000000000000000000000000000de0b6b3a7640000' + /// ``` + String encodeFunctionData(String function, [List? values]) => + impl.encodeFunctionData(function, values); + + /// Returns the encoded data, which can be used as the data for a transaction for [function] (see Specifying Fragments) for the given [values]. + /// + /// --- + /// + /// ```dart + /// // Encoding data for the tx.data of a call or transaction + /// iface.encodeFunctionData(iface.fragments.first, [ + /// '0x8ba1f109551bD432803012645Ac136ddd64DBA72', + /// '0xaB7C8803962c0f2F5BBBe3FA8bf41cd82AA1923C', + /// '1' + /// ]); + /// // '0x23b872dd0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72000000000000000000000000ab7c8803962c0f2f5bbbe3fa8bf41cd82aa1923c0000000000000000000000000000000000000000000000000de0b6b3a7640000' + /// ``` + String encodeFunctionDataFromFragment(Fragment function, + [List? values]) => + impl.encodeFunctionData(function.impl, values); /// Return the formatted [Interface]. /// - /// [types] must be from [FormatTypes] variable. + /// [type] must be from [FormatTypes] variable. /// /// If the format type is json a single string is returned, otherwise an Array of the human-readable strings is returned. - dynamic format([FormatTypes? types]) => - types != null ? impl.format(types.impl) : impl.format(); + dynamic format([FormatTypes? type]) => + type != null ? impl.format(type.impl) : impl.format(); /// Format into [FormatTypes.full]. /// @@ -82,15 +214,15 @@ class Interface extends Interop<_InterfaceImpl> { /// ''' /// [ /// { - /// "type": "function", - /// "name": "balanceOf", - /// "constant":true, - /// "stateMutability": "view", - /// "payable":false, "inputs": [ - /// { "type": "address", "name": "owner"} + /// 'type': 'function', + /// 'name': 'balanceOf', + /// 'constant':true, + /// 'stateMutability': 'view', + /// 'payable':false, 'inputs': [ + /// { 'type': 'address', 'name': 'owner'} /// ], - /// "outputs": [ - /// { "type": "uint256"} + /// 'outputs': [ + /// { 'type': 'uint256'} /// ] /// }, /// ] @@ -109,45 +241,58 @@ class Interface extends Interop<_InterfaceImpl> { /// ``` List formatMinimal() => (format(FormatTypes.minimal) as List).cast(); + /// Returns the [FunctionFragment] for [event]. + EventFragment getEvent(String event) => EventFragment._(impl.getEvent(event)); + + /// Returns the [FunctionFragment] for [event] fragment. + EventFragment getEventFromFragment(Fragment event) => + EventFragment._(impl.getEvent(event.impl)); + /// Return the topic hash for [event]. /// /// --- /// /// ```dart - /// iface.getEventTopic("Transfer"); + /// iface.getEventTopic('Transfer'); /// // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' /// - /// iface.getEventTopic("Transfer(address, address, uint)"); + /// iface.getEventTopic('Transfer(address, address, uint)'); /// // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' /// ``` String getEventTopic(String event) => impl.getEventTopic(event); + /// Returns the [FunctionFragment] for [function]. + FunctionFragment getFunction(String function) => + FunctionFragment._(impl.getFunction(function)); + + /// Returns the [FunctionFragment] for [function] fragment. + FunctionFragment getFunctionFromFragment(Fragment function) => + FunctionFragment._(impl.getFunction(function.impl)); + /// Return the sighash (or Function Selector) for [function]. /// /// --- /// /// ```dart - /// iface.getSighash("balanceOf"); + /// iface.getSighash('balanceOf'); /// // '0x70a08231' /// - /// iface.getSighash("balanceOf(address)"); + /// iface.getSighash('balanceOf(address)'); /// // '0x70a08231' /// ``` String getSighash(String function) => impl.getSighash(function); + /// Return the sighash (or Function Selector) for [fragment]. + /// + /// --- + /// + /// ```dart + /// iface.getSighash(iface.fragments.first); + /// // '0x70a08231' + /// ``` + String getSighashByFragment(Fragment fragment) => + impl.getSighash(fragment.impl); + @override String toString() => 'Interface: ${format(FormatTypes.minimal)}'; } - -extension _FormatTypesExtImpl on FormatTypes { - dynamic get impl { - switch (this) { - case FormatTypes.json: - return _FormatTypesImpl.json; - case FormatTypes.minimal: - return _FormatTypesImpl.minimal; - case FormatTypes.full: - return _FormatTypesImpl.full; - } - } -} diff --git a/lib/src/ethers/interop.dart b/lib/src/ethers/interop.dart index f2dc902..1c1e8bd 100644 --- a/lib/src/ethers/interop.dart +++ b/lib/src/ethers/interop.dart @@ -22,6 +22,17 @@ class _BlockWithTransactionImpl extends _RawBlockImpl { external List get transactions; } +@JS('utils.ConstructorFragment') +class _ConstructorFragmentImpl extends _FragmentImpl { + external BigNumber? get gas; + + external bool get payable; + + external String get stateMutability; + + external static _ConstructorFragmentImpl from(dynamic source); +} + @JS("Contract") class _ContractImpl { external _ContractImpl(String address, dynamic abi, dynamic providerOrSigner); @@ -34,6 +45,8 @@ class _ContractImpl { external _SignerImpl? get signer; + external _ContractImpl attach(String addressOrName); + external _ContractImpl connect(dynamic providerOrSigner); external int listenerCount([dynamic eventName]); @@ -60,6 +73,13 @@ class _EventFilterImpl { external set topics(List? topics); } +@JS('utils.EventFragment') +class _EventFragmentImpl extends _FragmentImpl { + external bool get anonymous; + + external static _EventFragmentImpl from(dynamic source); +} + @JS() @anonymous class _EventImpl extends _LogImpl { @@ -116,17 +136,61 @@ class _FormatTypesImpl { external static dynamic full; external static dynamic minimal; + + external static dynamic sighash; +} + +@JS('utils.Fragment') +class _FragmentImpl { + external List<_ParamTypeImpl> get inputs; + + external String? get name; + + external String get type; + + external String format([dynamic types]); + + external static _FragmentImpl from(String source); +} + +@JS('utils.FunctionFragment') +class _FunctionFragmentImpl extends _ConstructorFragmentImpl { + external bool get constant; + + external List<_ParamTypeImpl> get outputs; + + external String get stateMutability; + + external static _FunctionFragmentImpl from(dynamic source); } @JS("utils.Interface") class _InterfaceImpl { external _InterfaceImpl(dynamic abi); + external _ConstructorFragmentImpl get deploy; + + external dynamic get events; + + external List<_FragmentImpl> get fragments; + + external dynamic get functions; + + external List decodeFunctionResult(dynamic fragment, String data); + + external List encodeFilterTopics(dynamic fragment, List values); + + external String encodeFunctionData(dynamic fragment, [List? values]); + external dynamic format([dynamic types]); + external _EventFragmentImpl getEvent(dynamic fragment); + external String getEventTopic(String event); - external String getSighash(String function); + external _FunctionFragmentImpl getFunction(dynamic fragment); + + external String getSighash(dynamic function); } @JS("providers.JsonRpcProvider") @@ -177,6 +241,27 @@ class _NetworkImpl { external String get name; } +@JS('utils.ParamType') +class _ParamTypeImpl { + external _ParamTypeImpl? get arrayChildren; + + external int? get arrayLength; + + external String get baseType; + + external List<_ParamTypeImpl>? get components; + + external bool get indexed; + + external String? get name; + + external String? get type; + + external String format([dynamic types]); + + external static _ParamTypeImpl from(String source); +} + @JS("providers") class _ProviderImpl {} diff --git a/lib/src/ethers/signer.dart b/lib/src/ethers/signer.dart index 9cc1414..8b3eb47 100644 --- a/lib/src/ethers/signer.dart +++ b/lib/src/ethers/signer.dart @@ -8,25 +8,17 @@ part of ethers; class Signer extends Interop { const Signer._(_SignerImpl impl) : super.internal(impl as T); - /// Returns `true` if an only if object is a [Signer]. - static bool isSigner(Object object) { - if (object is Interop) - return object is Signer || _SignerImpl.isSigner(object.impl); - return false; - } - - Future _call(String method, [List args = const []]) async { - switch (T) { - case BigInt: - return (await _call(method, args)).toBigInt as T; - default: - return promiseToFuture(callMethod(impl, method, args)); - } - } + /// Returns the result of calling using the [request], with this account address being used as the from field. + Future call(TransactionRequest request) => + _call('call', [request.impl]); /// Connect this [Signer] to new [provider]. May simply throw an error if changing providers is not supported. Signer connect(Provider provider) => Signer._(impl.connect(provider.impl)); + /// Returns the result of estimating the cost to send the [request], with this account address being used as the from field. + Future estimateGas(TransactionRequest request) => + _call('estimateGas', [request.impl]); + /// Returns a Future that resolves to the account address. Future getAddress() => _call('getAddress'); @@ -52,10 +44,26 @@ class Signer extends Interop { /// /// The transaction must be valid (i.e. the nonce is correct and the account has sufficient balance to pay for the transaction). Future sendTransaction( - TransactionRequest request) async { - try { - return TransactionResponse._(await _call<_TransactionResponseImpl>( + TransactionRequest request) async => + TransactionResponse._(await _call<_TransactionResponseImpl>( 'sendTransaction', [request.impl])); + + /// Returns a Future which resolves to the Raw Signature of [message]. + Future signMessage(String message) => + _call('signMessage', [message]); + + /// Returns a Future which resolves to the signed transaction of the [request]. This method does not populate any missing fields. + Future signTransaction(TransactionRequest request) => + _call('signTransaction', [request.impl]); + + Future _call(String method, [List args = const []]) async { + try { + switch (T) { + case BigInt: + return (await _call(method, args)).toBigInt as T; + default: + return await promiseToFuture(callMethod(impl, method, args)); + } } catch (error) { final err = dartify(error); switch (err['code']) { @@ -80,19 +88,10 @@ class Signer extends Interop { } } - /// Returns the result of calling using the [request], with this account address being used as the from field. - Future call(TransactionRequest request) => - _call('call', [request.impl]); - - /// Returns the result of estimating the cost to send the [request], with this account address being used as the from field. - Future estimateGas(TransactionRequest request) => - _call('estimateGas', [request.impl]); - - /// Returns a Future which resolves to the signed transaction of the [request]. This method does not populate any missing fields. - Future signTransaction(TransactionRequest request) => - _call('signTransaction', [request.impl]); - - /// Returns a Future which resolves to the Raw Signature of [message]. - Future signMessage(String message) => - _call('signMessage', [message]); + /// Returns `true` if an only if object is a [Signer]. + static bool isSigner(Object object) { + if (object is Interop) + return object is Signer || _SignerImpl.isSigner(object.impl); + return false; + } } diff --git a/lib/src/utils/chains.dart b/lib/src/utils/chains.dart new file mode 100644 index 0000000..df3b88c --- /dev/null +++ b/lib/src/utils/chains.dart @@ -0,0 +1,154 @@ +import 'package:flutter_web3/src/ethereum/ethereum.dart'; + +enum Chains { + Mainnet, + Ropsten, + Rinkeby, + XDai, + Polygon, + Mumbai, + BSCMainnet, + BSCTestnet, +} + +extension ChainExtension on Chains { + static const _info = { + Chains.Mainnet: { + 'name': 'Ethereum Mainnet', + 'chain': 'ETH', + 'network': 'mainnet', + 'rpc': [], + 'nativeCurrency': {'name': 'Ether', 'symbol': 'ETH', 'decimals': 18}, + 'chainId': 1, + "explorers": ["https://etherscan.io/"], + 'multicall': '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', + }, + Chains.Ropsten: { + "name": "Ethereum Testnet Ropsten", + "chain": "ETH", + "network": "ropsten", + "rpc": [], + "nativeCurrency": { + "name": "Ropsten Ether", + "symbol": "ROP", + "decimals": 18 + }, + "chainId": 3, + "explorers": ["https://ropsten.etherscan.io/"], + 'multicall': '0x53c43764255c17bd724f74c4ef150724ac50a3ed', + }, + Chains.Rinkeby: { + "name": "Ethereum Testnet Rinkeby", + "chain": "ETH", + "network": "rinkeby", + "rpc": [], + "nativeCurrency": { + "name": "Rinkeby Ether", + "symbol": "RIN", + "decimals": 18 + }, + "chainId": 4, + "explorers": ["https://rinkeby.etherscan.io/"], + 'multicall': '0x42ad527de7d4e9d9d011ac45b31d8551f8fe9821', + }, + Chains.XDai: { + "name": "xDAI Chain", + "chain": "XDAI", + "network": "mainnet", + "rpc": [ + "https://rpc.xdaichain.com", + "https://xdai.poanetwork.dev", + "http://xdai.poanetwork.dev", + "https://dai.poa.network", + ], + "nativeCurrency": {"name": "xDAI", "symbol": "xDAI", "decimals": 18}, + "chainId": 100, + "explorers": ["https://blockscout.com/xdai/mainnet/"], + 'multicall': '0xb5b692a88bdfc81ca69dcb1d924f59f0413a602a', + }, + Chains.Polygon: { + "name": "Matic(Polygon) Mainnet", + "chain": "Matic(Polygon)", + "network": "mainnet", + "rpc": ['https://polygon-rpc.com/'], + "nativeCurrency": {"name": "Matic", "symbol": "MATIC", "decimals": 18}, + "chainId": 137, + "explorers": ["https://polygonscan.com/"], + 'multicall': '0x11ce4B23bD875D7F5C6a31084f55fDe1e9A87507', + }, + Chains.Mumbai: { + "name": "Matic(Polygon) Testnet Mumbai", + "chain": "Matic(Polygon)", + "network": "testnet", + "rpc": ["https://rpc-mumbai.matic.today"], + "faucets": ["https://faucet.matic.network/"], + "nativeCurrency": {"name": "Matic", "symbol": "tMATIC", "decimals": 18}, + "chainId": 80001, + "explorers": ["https://mumbai.polygonscan.com/"], + 'multicall': '0x08411ADd0b5AA8ee47563b146743C13b3556c9Cc', + }, + Chains.BSCMainnet: { + "name": "Binance Smart Chain Mainnet", + "chain": "BSC", + "network": "mainnet", + "rpc": [ + "https://bsc-dataseed1.binance.org", + "https://bsc-dataseed2.binance.org", + "https://bsc-dataseed3.binance.org", + "https://bsc-dataseed4.binance.org", + ], + "nativeCurrency": { + "name": "Binance Chain Native Token", + "symbol": "BNB", + "decimals": 18 + }, + "chainId": 56, + "explorers": ["https://bscscan.com"], + 'multicall': '0x41263cba59eb80dc200f3e2544eda4ed6a90e76c', + }, + Chains.BSCTestnet: { + "name": "Binance Smart Chain Testnet", + "chain": "BSC", + "network": "Chapel", + "rpc": [ + "https://data-seed-prebsc-1-s1.binance.org:8545", + "https://data-seed-prebsc-2-s1.binance.org:8545", + "https://data-seed-prebsc-1-s2.binance.org:8545", + "https://data-seed-prebsc-2-s2.binance.org:8545", + "https://data-seed-prebsc-1-s3.binance.org:8545", + "https://data-seed-prebsc-2-s3.binance.org:8545" + ], + "faucets": ["https://testnet.binance.org/faucet-smart"], + "nativeCurrency": { + "name": "Binance Chain Native Token", + "symbol": "tBNB", + "decimals": 18 + }, + "chainId": 97, + "explorers": ["https://testnet.bscscan.com"], + 'multicall': '0xae11C5B5f29A6a25e955F0CB8ddCc416f522AF5C', + }, + }; + + String? get multicallAddress => _info[this]!['multicall'] as String?; + + String get name => _info[this]!['name'] as String; + + String get chain => _info[this]!['chain'] as String; + + String get network => _info[this]!['network'] as String; + + int get chainId => _info[this]!['chainId'] as int; + + List get rpc => _info[this]!['rpc'] as List; + + List get explorers => _info[this]!['explorers'] as List; + + List? get faucets => _info[this]!['faucets'] as List?; + + CurrencyParams get nativeCurrency { + final info = _info[this]!['nativeCurrency']! as Map; + return CurrencyParams( + name: info['name'], symbol: info['symbol'], decimals: info['decimals']); + } +} diff --git a/lib/src/utils/erc1155.dart b/lib/src/utils/erc1155.dart new file mode 100644 index 0000000..ab5e91e --- /dev/null +++ b/lib/src/utils/erc1155.dart @@ -0,0 +1,235 @@ +import 'dart:async'; + +import '../ethers/ethers.dart'; + +/// Dart Class for ERC1155 Contract, A standard API for fungibility-agnostic and gas-efficient tokens within smart contracts. +class ContractERC1155 { + /// Minimal abi interface of ERC1155 + static const abi = [ + 'function balanceOf(address,uint) view returns (uint)', + 'function balanceOfBatch(address[],uint[]) view returns (uint[])', + 'function uri(uint) view returns (string)', + 'function isApprovedForAll(address owner, address spender) view returns (bool)', + 'function setApprovedForAll(address spender, bool approved)', + 'function safeTransferFrom(address, address, uint, uint, bytes)', + 'function safeBatchTransferFrom(address, address, uint[], uint[], bytes)', + 'function totalSupply(uint256 id) view returns (uint256)', + 'function exists(uint256 id) view returns (bool)', + 'function burn(address account, uint256 id, uint256 value)', + 'function burnBatch(address account, uint256[] ids, uint256[] values)', + 'event ApprovalForAll(address indexed account, address indexed operator, bool approved)', + 'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)', + 'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)', + ]; + + /// Ethers Contract object. + Contract contract; + + String _uri = ''; + + /// Instantiate ERC1155 Contract using default abi if [abi] is not `null`. + ContractERC1155(String address, dynamic providerOrSigner, [dynamic abi]) + : assert(providerOrSigner != null, 'providerOrSigner should not be null'), + assert(address.isNotEmpty, 'address should not be empty'), + assert( + EthUtils.isAddress(address), 'address should be in address format'), + contract = + Contract(address, abi ?? ContractERC1155.abi, providerOrSigner); + + /// [Log] of `ApprovalForAll` events. + Future> approvalForAllEvents( + [List? args, dynamic startBlock, dynamic endBlock]) => + contract.queryFilter(contract.getFilter('ApprovalForAll', args ?? []), + startBlock, endBlock); + + /// Returns the amount of tokens [id] owned by [address] + Future balanceOf(String address, int id) async => + contract.call('balanceOf', [address, id]); + + /// Returns the amount of tokens [ids] owned by [addresses] + Future> balanceOfBatch( + List addresses, List ids) async => + (await contract.call('balanceOfBatch', [addresses, ids])) + .cast() + .map((e) => e.toBigInt) + .toList(); + + /// Returns the amount of tokens [ids] owned by [address] + Future> balanceOfBatchSingleAddress( + String address, List ids) async => + (await contract.call( + 'balanceOfBatch', + [ + List.generate(ids.length, (index) => address), + ids, + ], + )) + .cast() + .map((e) => e.toBigInt) + .toList(); + + /// Connect current [contract] with [providerOrSigner] + void connect(dynamic providerOrSigner) { + assert(providerOrSigner is Provider || providerOrSigner is Signer); + contract = contract.connect(providerOrSigner); + } + + /// Returns `true` if [spender] is approved to transfer [owner] tokens + Future isApprovedForAll(String owner, String spender) async => + contract.call('isApprovedForAll', [owner, spender]); + + /// Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to `approved`. + void onApprovalForAll( + void Function( + String account, + String operator, + Event event, + ) + callback, + ) => + contract.on( + 'ApprovalForAll', + (String account, String operator, dynamic data) => callback( + account, + operator, + Event.fromJS(data), + ), + ); + + /// Equivalent to multiple `TransferSingle` events, where `operator`, `from` and `to` are the same for all transfers. + void onTransferBatch( + void Function( + String operator, + String from, + String to, + Event event, + ) + callback, + ) => + contract.on( + 'TransferBatch', + (String operator, String from, String to, dynamic data) => callback( + operator, + from, + to, + Event.fromJS(data), + ), + ); + + /// Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. + void onTransferSingle( + void Function( + String operator, + String from, + String to, + Event event, + ) + callback, + ) => + contract.on( + 'TransferSingle', + (String operator, String from, String to, dynamic data) => callback( + operator, + from, + to, + Event.fromJS(data), + ), + ); + + /// Batched version of [safeTransferFrom]. + Future safeBatchTransferFrom( + String from, + String to, + List id, + List amount, + String data, + ) => + contract.send('safeBatchTransferFrom', + [from, to, id, amount.map((e) => e.toString()).toList(), data]); + + /// Transfers [amount] tokens of token type [id] from [from] to [to]. + Future safeTransferFrom( + String from, + String to, + int id, + BigInt amount, + String data, + ) => + contract.send('safeTransferFrom', [from, to, id, amount, data]); + + /// Grants or revokes permission to [spender] to transfer the caller's tokens, according to [approved], + Future setApprovedForAll( + String spender, bool approved) => + contract.send('setApprovedForAll', [spender, approved]); + + /// [Log] of `TransferBatch` events. + Future> transferBatchEvents( + [List? args, dynamic startBlock, dynamic endBlock]) => + contract.queryFilter(contract.getFilter('TransferBatch', args ?? []), + startBlock, endBlock); + + /// [Log] of `TransferSingle` events. + Future> transferSingleEvents( + [List? args, dynamic startBlock, dynamic endBlock]) => + contract.queryFilter(contract.getFilter('TransferSingle', args ?? []), + startBlock, endBlock); + + /// Returns the URI for token type [id]. + /// + /// This will also replace `{id}` in original uri fetched by [id]. + FutureOr uri(int id) async { + if (_uri.isEmpty) _uri = await contract.call('uri', [id]); + return _uri.replaceAll('{id}', id.toString()); + } +} + +/// Dart Class for ERC1155Burnable Contract that allows token holders to destroy both their own tokens and those that they have been approved to use. +class ContractERC1155Burnable extends ContractERC1155 with ERC1155Supply { + /// Instantiate ERC1155 Contract using default abi if [abi] is not `null`. + ContractERC1155Burnable(String address, dynamic providerOrSigner, + [dynamic abi]) + : super(address, providerOrSigner, abi); +} + +/// Dart Class for ERC1155Supply Contract that adds tracking of total supply per id to normal ERC1155. +class ContractERC1155Supply extends ContractERC1155 with ERC1155Supply { + /// Instantiate ERC1155 Contract using default abi if [abi] is not `null`. + ContractERC1155Supply(String address, dynamic providerOrSigner, [dynamic abi]) + : super(address, providerOrSigner, abi); +} + +/// Dart Class for both [ContractERC1155Supply] and [ContractERC1155Burnable] combined. +class ContractERC1155SupplyBurnable extends ContractERC1155 + with ERC1155Supply, ERC1155Burnable { + /// Instantiate ERC1155 Contract using default abi if [abi] is not `null`. + ContractERC1155SupplyBurnable(String address, dynamic providerOrSigner, + [dynamic abi]) + : super(address, providerOrSigner, abi); +} + +/// Dart Mixin for ERC1155Burnable that allows token holders to destroy both their own tokens and those that they have been approved to use. +mixin ERC1155Burnable on ContractERC1155 { + Future burn(String address, int id, BigInt value) => + contract.send('burn', [address, id, value.toString()]); + + Future burnBatch( + String address, List ids, List values) => + contract.send('burnBatch', [ + address, + ids, + values.map((e) => e.toString()).toList(), + ]); +} + +/// Dart Mixin for ERC1155Supply that adds tracking of total supply per id to normal ERC1155. +mixin ERC1155Supply on ContractERC1155 { + /// Indicates weither any token exist with a given [id], or not. + Future exists(int id) async { + return contract.call('exists', [id]); + } + + /// Total amount of tokens in with a given [id]. + Future totalSupply(int id) async { + return contract.call('totalSupply', [id]); + } +} diff --git a/lib/src/ethers/utils.dart b/lib/src/utils/erc20.dart similarity index 78% rename from lib/src/ethers/utils.dart rename to lib/src/utils/erc20.dart index 13729c3..7e2408c 100644 --- a/lib/src/ethers/utils.dart +++ b/lib/src/utils/erc20.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'ethers.dart'; +import '../ethers/ethers.dart'; +import 'multicall.dart'; /// Dart Class for ERC20 Contract, A standard API for tokens within smart contracts. /// @@ -106,23 +107,56 @@ class ContractERC20 { contract = contract.connect(providerOrSigner); } - /// Multicall of [allowance], may not be in the same block. + /// Multicall of [allowance], may not be in the same block unless [multicall] is provided. Future> multicallAllowance( - List owners, List spenders) async { + List owners, + List spenders, [ + Multicall? multicall, + ]) async { assert(owners.isNotEmpty, 'Owner list empty'); assert(spenders.isNotEmpty, 'Spender list empty'); assert(owners.length == spenders.length, 'Owner list length must be same as spender'); - return Future.wait(Iterable.generate(owners.length).map( - (e) => allowance(owners[e], spenders[e]), - )); + if (multicall != null) { + final res = + await multicall.aggregate(Iterable.generate(owners.length).map( + (e) { + final functionSig = contract.interface.getSighash('allowance'); + final argData = abiCoder.encode( + ['address', 'address'], [owners[e], spenders[e]]).substring(2); + return MulticallPayload(contract.address, functionSig + argData); + }, + ).toList()); + return res.returnData.map((e) => BigInt.parse(e)).toList(); + } else { + return Future.wait(Iterable.generate(owners.length).map( + (e) => allowance(owners[e], spenders[e]), + )); + } } - /// Multicall of [balanceOf], may not be in the same block. - Future> multicallBalanceOf(List addresses) async { + /// Multicall of [balanceOf], may not be in the same block unless [multicall] is provided. + Future> multicallBalanceOf( + List addresses, [ + Multicall? multicall, + ]) async { assert(addresses.isNotEmpty, 'address should not be empty'); - return Future.wait(Iterable.generate(addresses.length) - .map((e) => balanceOf(addresses[e]))); + + if (multicall != null) { + final res = await multicall + .aggregate(Iterable.generate(addresses.length).map( + (e) { + final functionSig = contract.interface.getSighash('balanceOf'); + final argData = + abiCoder.encode(['address'], [addresses[e]]).substring(2); + return MulticallPayload(contract.address, functionSig + argData); + }, + ).toList()); + return res.returnData.map((e) => BigInt.parse(e)).toList(); + } else { + return Future.wait(Iterable.generate(addresses.length) + .map((e) => balanceOf(addresses[e]))); + } } /// Emitted when the allowance of a `spender` for an `owner` is set by a call to `approve`. diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart new file mode 100644 index 0000000..84fb153 --- /dev/null +++ b/lib/src/utils/extensions.dart @@ -0,0 +1,17 @@ +import '../ethereum/ethereum.dart'; +import 'chains.dart'; + +extension EtherumChainExt on Ethereum { + /// Use `Ethereum.walletSwitchChain` function with [chain] information. RPC Url list in [chain] will be overridden if [rpcs] is not `null`. + Future walletSwitchChainByChains(Chains chain, [List? rpcs]) => + walletSwitchChain( + chain.chainId, + () => walletAddChain( + chainId: chain.chainId, + chainName: chain.name, + nativeCurrency: chain.nativeCurrency, + rpcUrls: rpcs ?? chain.rpc, + blockExplorerUrls: chain.explorers, + ), + ); +} diff --git a/lib/src/utils/multicall.dart b/lib/src/utils/multicall.dart new file mode 100644 index 0000000..06c69bd --- /dev/null +++ b/lib/src/utils/multicall.dart @@ -0,0 +1,121 @@ +import '../ethers/ethers.dart'; +import 'chains.dart'; + +class Multicall { + static const abi = [ + 'function aggregate((address, bytes)[]) view returns (uint256, bytes[])', + ]; + + Contract contract; + + Multicall(String multicallAddress, dynamic providerOrSigner) + : assert(providerOrSigner != null, 'providerOrSigner should not be null'), + assert(multicallAddress.isNotEmpty, 'address should not be empty'), + assert(EthUtils.isAddress(multicallAddress), + 'address should be in address format'), + contract = Contract(multicallAddress, abi, providerOrSigner); + + factory Multicall.fromChain(Chains chain, providerOrSigner) { + assert(chain.multicallAddress != null, + 'Multicall not supported on this chain'); + return Multicall(chain.multicallAddress!, providerOrSigner); + } + + Future aggregate(List payload) async { + assert(payload.isNotEmpty, 'payload should not be empty'); + + final res = await contract.call('aggregate', [ + payload.map((e) => e.serialize()).toList(), + ]); + return MulticallResult( + int.parse(res[0].toString()), (res[1] as List).cast()); + } + + Future> multipleERC20Balances( + List tokens, + List addresses, + ) async { + assert(addresses.isNotEmpty && tokens.isNotEmpty, + 'addresses and tokens should not be empty'); + assert(addresses.length == tokens.length, + 'addresses and tokens length should be equal'); + + final payload = new Iterable.generate(tokens.length) + .map( + (e) => MulticallPayload.fromFunctionAbi( + tokens[e], + 'function balanceOf(address) view returns (uint)', + [addresses[e]], + ), + ) + .toList(); + + final res = await aggregate(payload); + + return res.returnData.map((e) => BigInt.parse(e)).toList(); + } + + Future> multipleERC20Allowance( + List tokens, + List owners, + List spenders, + ) async { + assert(tokens.isNotEmpty && owners.isNotEmpty && spenders.isNotEmpty); + assert(tokens.length == owners.length && tokens.length == spenders.length); + + final payload = new Iterable.generate(tokens.length) + .map( + (e) => MulticallPayload.fromFunctionAbi( + tokens[e], + 'function allowance(address owner, address spender) external view returns (uint256)', + [owners[e], spenders[e]], + ), + ) + .toList(); + + final res = await aggregate(payload); + + return res.returnData.map((e) => BigInt.parse(e)).toList(); + } +} + +class MulticallPayload { + final String address; + final String data; + + const MulticallPayload(this.address, this.data); + + factory MulticallPayload.fromFunctionAbi(String address, String functionAbi, + [List? args]) { + final interface = Interface([functionAbi]); + final data = interface.encodeFunctionDataFromFragment( + interface.fragments.first, args); + return MulticallPayload(address, data); + } + + factory MulticallPayload.fromInterfaceFunction( + String address, Interface interface, String function, + [List? args]) { + final data = interface.encodeFunctionData(function, args); + return MulticallPayload(address, data); + } + + List serialize() => [address, data]; + + @override + String toString() { + return 'MulticallPayload: to $address with data $data'; + } +} + +class MulticallResult { + final int blockNumber; + final List returnData; + + const MulticallResult(this.blockNumber, this.returnData); + + @override + String toString() { + return 'MulticallResult: at block $blockNumber total ${returnData.length} item, ${returnData.take(3)}...'; + } +} diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..3732a9f --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,5 @@ +export 'src/utils/chains.dart'; +export 'src/utils/erc1155.dart'; +export 'src/utils/erc20.dart'; +export 'src/utils/extensions.dart'; +export 'src/utils/multicall.dart'; diff --git a/pubspec.lock b/pubspec.lock index 91cdc9c..e18f5d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + amdjs: + dependency: "direct main" + description: + name: amdjs + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.1" boolean_selector: dependency: transitive description: @@ -28,7 +42,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -43,6 +57,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + dom_tools: + dependency: transitive + description: + name: dom_tools + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + enum_to_string: + dependency: transitive + description: + name: enum_to_string + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" fake_async: dependency: transitive description: @@ -60,6 +88,20 @@ packages: description: flutter source: sdk version: "0.0.0" + html_unescape: + dependency: transitive + description: + name: html_unescape + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" js: dependency: "direct main" description: @@ -67,6 +109,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" + json_object_mapper: + dependency: transitive + description: + name: json_object_mapper + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" matcher: dependency: transitive description: @@ -80,7 +136,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: @@ -88,6 +144,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + resource_portable: + dependency: transitive + description: + name: resource_portable + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -121,6 +184,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + swiss_knife: + dependency: transitive + description: + name: swiss_knife + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.8" term_glyph: dependency: transitive description: @@ -134,7 +204,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b182037..2545948 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_web3 description: Web3 Ethereum, Etherjs and Wallet Connect wrapper for Flutter Web. Made especially for developing Dapp. -version: 2.1.9 +version: 2.2.0-pre.7 repository: https://github.com/y-pakorn/flutter_web3 issue_tracker: https://github.com/y-pakorn/flutter_web3/issues @@ -13,6 +13,7 @@ dependencies: sdk: flutter js: ^0.6.3 meta: ^1.3.0 + amdjs: ^2.0.1 dev_dependencies: flutter_test: