From a9631db8c888a6a1571f0cdddd0507e662342178 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 6 Jan 2023 00:40:08 +1100 Subject: [PATCH 01/26] added rpc multiquery service functionality --- lib/json_rpc_multiquery.dart | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 lib/json_rpc_multiquery.dart diff --git a/lib/json_rpc_multiquery.dart b/lib/json_rpc_multiquery.dart new file mode 100644 index 00000000..4e7abe66 --- /dev/null +++ b/lib/json_rpc_multiquery.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +import 'json_rpc.dart'; + +class JsonRPCMultiQuery extends JsonRPC { + JsonRPCMultiQuery(String url, Client client) : super(url, client); + + int _currentRequestId = 1; + + Future> callMultiQuery(List queries) async { + final payloadList = >[]; + for (final query in queries) { + payloadList.add( + { + 'jsonrpc': '2.0', + 'method': query.function, + 'params': query.params ?? [], + 'id': _currentRequestId++, + }, + ); + } + final response = await client.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: json.encode(payloadList), + ); + + final responses = []; + + final dataList = json.decode(response.body) as List; + final castedList = dataList.cast>(); + + for (final data in castedList) { + if (data.containsKey('error')) { + final error = data['error']; + + final code = error['code'] as int; + final message = error['message'] as String; + final errorData = error['data']; + + throw RPCError(code, message, errorData); + } + + final id = data['id'] as int; + final result = data['result']; + responses.add(RPCResponse(id, result)); + } + return responses; + } +} + +class RPCQuery { + RPCQuery(this.function, [this.params]); + + final String function; + final List? params; +} From 8012a7f1bc6dd0e05724ad0e480cabf32dadd38c Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 6 Jan 2023 00:40:24 +1100 Subject: [PATCH 02/26] tests for rpc multiquery service --- test/json_rpc_multiquery_test.dart | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 test/json_rpc_multiquery_test.dart diff --git a/test/json_rpc_multiquery_test.dart b/test/json_rpc_multiquery_test.dart new file mode 100644 index 00000000..0deb2059 --- /dev/null +++ b/test/json_rpc_multiquery_test.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:test/test.dart'; +import 'package:web3dart/json_rpc_multiquery.dart'; + +final uri = Uri.parse('url'); + +void main() { + late MockClient client; + + setUp(() { + client = MockClient(); + }); + + test('encodes and sends requests', () async { + final queries = [ + RPCQuery('eth_gasPrice'), + RPCQuery( + 'eth_getBalance', + ['0x95222290dd7278aa32dd189cc1e1d165cc4bafe5'], + ) + ]; + await JsonRPCMultiQuery('url', client).callMultiQuery(queries); + + final request = client.request!; + expect( + request.headers, + containsPair('Content-Type', startsWith('application/json')), + ); + }); + + test('increments request id', () async { + final rpc = JsonRPCMultiQuery('url', client); + final queries = [ + RPCQuery('eth_gasPrice'), + RPCQuery( + 'eth_getBalance', + ['0x95222290dd7278aa32dd189cc1e1d165cc4bafe5'], + ) + ]; + await rpc.callMultiQuery(queries); + + final lastRequest = client.request!; + expect( + lastRequest.finalize().bytesToString(), + completion(contains('"id":2')), + ); + }); + + test('throws errors', () { + final rpc = JsonRPCMultiQuery('url', client); + client.nextResponse = StreamedResponse( + Stream.value( + utf8.encode( + '[' + '{"id": 1, "jsonrpc": "2.0", ' + '"error": {"code": 1, "message": "Message", "data": "data"}}, ' + '{"id": 2, "jsonrpc": "2.0", ' + '"error": {"code": 1, "message": "Message", "data": "data"}}' + ']', + ), + ), + 200, + ); + + expect( + rpc.callMultiQuery( + [RPCQuery('eth_gasPrice')], + ), + throwsException, + ); + }); +} + +class MockClient extends BaseClient { + StreamedResponse? nextResponse; + BaseRequest? request; + + @override + Future send(BaseRequest request) { + this.request = request; + return Future.value( + nextResponse ?? + StreamedResponse( + Stream.value( + utf8.encode( + '[{"id": 1, "jsonrpc": "2.0", "result": "0x1"},' + '{"id": 2, "jsonrpc": "2.0", "result": "0x1"}]', + ), + ), + 200, + ), + ); + } +} From 321b755d49b9fe6061c3000cb86d6a343641b914 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 7 Jan 2023 13:40:42 +1100 Subject: [PATCH 03/26] added optional id to rpcError class for identifing mulitple queries responses --- lib/json_rpc.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/json_rpc.dart b/lib/json_rpc.dart index 98c1ff75..90f9064f 100644 --- a/lib/json_rpc.dart +++ b/lib/json_rpc.dart @@ -78,8 +78,9 @@ class RPCResponse { /// Exception thrown when an the server returns an error code to an rpc request. class RPCError implements Exception { - const RPCError(this.errorCode, this.message, this.data); + const RPCError(this.errorCode, this.message, this.data, [this.id]); + final int? id; final int errorCode; final String message; final dynamic data; From c1f0af96e575fc0ce453905ca0fbc71d758a74e2 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 7 Jan 2023 13:46:01 +1100 Subject: [PATCH 04/26] documented multiqueryRpcService interface instead of throwing an RPCError, now an error is returned in the list of responses, to be handled by the receiver appropiately --- lib/json_rpc_multiquery.dart | 67 +++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/lib/json_rpc_multiquery.dart b/lib/json_rpc_multiquery.dart index 4e7abe66..e848a527 100644 --- a/lib/json_rpc_multiquery.dart +++ b/lib/json_rpc_multiquery.dart @@ -1,15 +1,35 @@ +library json_rpc_multiquery; + import 'dart:convert'; import 'package:http/http.dart'; +import 'contracts.dart'; +import 'credentials.dart'; +import 'crypto.dart'; import 'json_rpc.dart'; +import 'src/core/block_number.dart'; + +abstract class MultiQueryRpcService { + /// Performs a single RPC request, asking the server to execute several queries + /// using the functions with associated parameters for each one, the parameters + /// need to be encodable with the [json] class of dart:convert. + /// + /// When the request is successful, a list is returned, containing each query + /// response. This responses can be either an [RPCResponse] on success, or an + /// [RPCError] on failure. + /// No [RPCError] instances will be thrown, they will only be part of the list. + /// Other errors might be thrown if an IO-Error occurs. + Future> callMultiQuery(List queries); +} -class JsonRPCMultiQuery extends JsonRPC { +class JsonRPCMultiQuery extends JsonRPC implements MultiQueryRpcService { JsonRPCMultiQuery(String url, Client client) : super(url, client); int _currentRequestId = 1; - Future> callMultiQuery(List queries) async { + @override + Future> callMultiQuery(List queries) async { final payloadList = >[]; for (final query in queries) { payloadList.add( @@ -27,20 +47,22 @@ class JsonRPCMultiQuery extends JsonRPC { body: json.encode(payloadList), ); - final responses = []; + // responses list will be RPCResponse and/or RPCError instances + final responses = []; final dataList = json.decode(response.body) as List; final castedList = dataList.cast>(); for (final data in castedList) { if (data.containsKey('error')) { + final id = data['id'] as int; final error = data['error']; final code = error['code'] as int; final message = error['message'] as String; final errorData = error['data']; - throw RPCError(code, message, errorData); + responses.add(RPCError(code, message, errorData, id)); } final id = data['id'] as int; @@ -52,8 +74,43 @@ class JsonRPCMultiQuery extends JsonRPC { } class RPCQuery { - RPCQuery(this.function, [this.params]); + RPCQuery(this.function, [this.params, this.id]); final String function; + final String? id; final List? params; } + +/// Useful class to +class EthContractCall { + EthContractCall({ + this.sender, + required this.contract, + required this.function, + required this.params, + this.atBlock = const BlockNum.current(), + }); + + final EthereumAddress? sender; + final DeployedContract contract; + final ContractFunction function; + final List params; + final BlockNum? atBlock; + + RPCQuery toRPCQuery(String id) { + final data = function.encodeCall(params); + final rawParamsBody = { + 'to': contract.address.hex, + 'data': bytesToHex(data, include0x: true, padToEvenLength: true), + if (sender != null) 'from': sender!.hex, + }; + return RPCQuery( + 'eth_call', + [ + rawParamsBody, + atBlock, + ], + id, + ); + } +} From 89ad692cca8f72350b36a32d1896b929f88fea1d Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 7 Jan 2023 13:47:42 +1100 Subject: [PATCH 05/26] implemented multiquery client with method multiquery call --- lib/src/core/multiquery_client.dart | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 lib/src/core/multiquery_client.dart diff --git a/lib/src/core/multiquery_client.dart b/lib/src/core/multiquery_client.dart new file mode 100644 index 00000000..022de040 --- /dev/null +++ b/lib/src/core/multiquery_client.dart @@ -0,0 +1,49 @@ +part of 'package:web3dart/web3dart.dart'; + +class MultiQueryWeb3Client extends Web3Client { + MultiQueryWeb3Client( + String url, + Client httpClient, { + SocketConnector? socketConnector, + }) : super.custom( + JsonRPCMultiQuery(url, httpClient), + socketConnector: socketConnector, + ); + + Future> multiqueryCall( + List contractCalls, + List rawQuerys, + ) async { + // Each instance of contract call is mapped to an id (the index). + // This is intended to later find out how to decode the returned values. + final contractCallsMap = contractCalls.asMap(); + int lastId = contractCallsMap.length; + + final contractQueries = contractCallsMap.entries.map( + (e) => e.value.toRPCQuery( + e.key.toString(), + ), + ); + final rawQuerysWithId = rawQuerys.map( + (q) => RPCQuery(q.function, q.params, q.id ?? (lastId++).toString()), + ); + + final responses = await (_jsonRpc as MultiQueryRpcService).callMultiQuery([ + ...contractQueries, + ...rawQuerysWithId, + ]); + final decodedResponses = []; + for (final res in responses) { + // each response can be either an error or a correct value returned + if (res is RPCResponse) { + final function = contractCallsMap[res.id]!.function; + final decodedResult = function.decodeReturnValues(res.result as String); + decodedResponses.add(decodedResult); + } else if (res is RPCError) { + decodedResponses.add(res); + } + } + + return decodedResponses; + } +} From 079977b7ae97bd43db93e2fbde09cc38c1e79bfd Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 7 Jan 2023 13:47:47 +1100 Subject: [PATCH 06/26] added multiquery client to main librart exports --- lib/web3dart.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/web3dart.dart b/lib/web3dart.dart index f0954a89..86ad1ab9 100644 --- a/lib/web3dart.dart +++ b/lib/web3dart.dart @@ -14,6 +14,7 @@ import 'contracts.dart'; import 'credentials.dart'; import 'crypto.dart'; import 'json_rpc.dart'; +import 'json_rpc_multiquery.dart'; import 'src/core/amount.dart'; import 'src/core/block_information.dart'; import 'src/core/block_number.dart'; @@ -29,6 +30,7 @@ export 'src/core/block_information.dart'; export 'src/core/block_number.dart'; export 'src/core/sync_information.dart'; +part 'src/core/multiquery_client.dart'; part 'src/core/client.dart'; part 'src/core/filters.dart'; part 'src/core/transaction.dart'; From f0dca94945b68a466fa55d756cb811f3f9daf17d Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 7 Jan 2023 14:19:34 +1100 Subject: [PATCH 07/26] fixed not using query id attribute for rpc body preparation --- lib/json_rpc_multiquery.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/json_rpc_multiquery.dart b/lib/json_rpc_multiquery.dart index e848a527..af63a647 100644 --- a/lib/json_rpc_multiquery.dart +++ b/lib/json_rpc_multiquery.dart @@ -37,7 +37,7 @@ class JsonRPCMultiQuery extends JsonRPC implements MultiQueryRpcService { 'jsonrpc': '2.0', 'method': query.function, 'params': query.params ?? [], - 'id': _currentRequestId++, + 'id': query.id ?? _currentRequestId++, }, ); } From 11d58390a261ccfdb7160ee284812d6d8868e284 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 7 Jan 2023 14:19:45 +1100 Subject: [PATCH 08/26] adjusted and added some tests --- test/json_rpc_multiquery_test.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/json_rpc_multiquery_test.dart b/test/json_rpc_multiquery_test.dart index 0deb2059..a3973927 100644 --- a/test/json_rpc_multiquery_test.dart +++ b/test/json_rpc_multiquery_test.dart @@ -3,10 +3,9 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:test/test.dart'; +import 'package:web3dart/json_rpc.dart'; import 'package:web3dart/json_rpc_multiquery.dart'; -final uri = Uri.parse('url'); - void main() { late MockClient client; @@ -25,13 +24,23 @@ void main() { await JsonRPCMultiQuery('url', client).callMultiQuery(queries); final request = client.request!; + expect( request.headers, containsPair('Content-Type', startsWith('application/json')), ); + + expect( + request, + isA(), + ); + + final expectedBody = + '[{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":1},{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x95222290dd7278aa32dd189cc1e1d165cc4bafe5"],"id":2}]'; + expect((request as Request).body, equals(expectedBody)); }); - test('increments request id', () async { + test('automatically increments request id when none provided', () async { final rpc = JsonRPCMultiQuery('url', client); final queries = [ RPCQuery('eth_gasPrice'), @@ -49,7 +58,7 @@ void main() { ); }); - test('throws errors', () { + test('returns errors', () { final rpc = JsonRPCMultiQuery('url', client); client.nextResponse = StreamedResponse( Stream.value( @@ -69,7 +78,7 @@ void main() { rpc.callMultiQuery( [RPCQuery('eth_gasPrice')], ), - throwsException, + completion(anyElement(isA())), ); }); } From 0fd3ea03bd01dece5b89b757e1ec0ef9df5ff267 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 13 Jan 2023 03:56:52 +1100 Subject: [PATCH 09/26] added new handy EtherAmount constructor from hex --- lib/src/core/amount.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/core/amount.dart b/lib/src/core/amount.dart index 2478a52d..091336aa 100644 --- a/lib/src/core/amount.dart +++ b/lib/src/core/amount.dart @@ -1,3 +1,5 @@ +import 'package:web3dart/crypto.dart'; + enum EtherUnit { ///Wei, the smallest and atomic amount of Ether wei, @@ -45,6 +47,12 @@ class EtherAmount { return EtherAmount.inWei(parsedAmount * _factors[unit]!); } + /// Constructs an amount of Ether in wei from an hex String + factory EtherAmount.fromHex(String hex) { + final amountInBigInt = hexToInt(hex); + return EtherAmount.inWei(amountInBigInt); + } + /// Gets the value of this amount in the specified unit as a whole number. /// **WARNING**: For all units except for [EtherUnit.wei], this method will /// discard the remainder occurring in the division, making it unsuitable for From 07f8fae7ee69ea8f3b7cb832a6fada4368756d2d Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 13 Jan 2023 03:58:00 +1100 Subject: [PATCH 10/26] cleaned json rpc multiquery class --- lib/json_rpc_multiquery.dart | 40 ++---------------------------------- lib/web3dart.dart | 1 + 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/lib/json_rpc_multiquery.dart b/lib/json_rpc_multiquery.dart index af63a647..fc68b4c7 100644 --- a/lib/json_rpc_multiquery.dart +++ b/lib/json_rpc_multiquery.dart @@ -1,14 +1,12 @@ +// ignore_for_file: sort_constructors_first + library json_rpc_multiquery; import 'dart:convert'; import 'package:http/http.dart'; -import 'contracts.dart'; -import 'credentials.dart'; -import 'crypto.dart'; import 'json_rpc.dart'; -import 'src/core/block_number.dart'; abstract class MultiQueryRpcService { /// Performs a single RPC request, asking the server to execute several queries @@ -80,37 +78,3 @@ class RPCQuery { final String? id; final List? params; } - -/// Useful class to -class EthContractCall { - EthContractCall({ - this.sender, - required this.contract, - required this.function, - required this.params, - this.atBlock = const BlockNum.current(), - }); - - final EthereumAddress? sender; - final DeployedContract contract; - final ContractFunction function; - final List params; - final BlockNum? atBlock; - - RPCQuery toRPCQuery(String id) { - final data = function.encodeCall(params); - final rawParamsBody = { - 'to': contract.address.hex, - 'data': bytesToHex(data, include0x: true, padToEvenLength: true), - if (sender != null) 'from': sender!.hex, - }; - return RPCQuery( - 'eth_call', - [ - rawParamsBody, - atBlock, - ], - id, - ); - } -} diff --git a/lib/web3dart.dart b/lib/web3dart.dart index 86ad1ab9..552155fd 100644 --- a/lib/web3dart.dart +++ b/lib/web3dart.dart @@ -18,6 +18,7 @@ import 'json_rpc_multiquery.dart'; import 'src/core/amount.dart'; import 'src/core/block_information.dart'; import 'src/core/block_number.dart'; +import 'src/core/eth_rpc_query/eth_rpc_query.dart'; import 'src/core/sync_information.dart'; import 'src/utils/rlp.dart' as rlp; import 'src/utils/typed_data.dart'; From ea716ceb51c8178e9c587e008ff9036148e47eff Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 13 Jan 2023 04:00:05 +1100 Subject: [PATCH 11/26] eth rpc query handy class for handling specific ethereum rpc queries created some really handy constructors to be able to create several queries easily --- lib/src/core/eth_rpc_query/eth_rpc_query.dart | 50 ++++ lib/src/core/eth_rpc_query/factories.dart | 258 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 lib/src/core/eth_rpc_query/eth_rpc_query.dart create mode 100644 lib/src/core/eth_rpc_query/factories.dart diff --git a/lib/src/core/eth_rpc_query/eth_rpc_query.dart b/lib/src/core/eth_rpc_query/eth_rpc_query.dart new file mode 100644 index 00000000..5877720e --- /dev/null +++ b/lib/src/core/eth_rpc_query/eth_rpc_query.dart @@ -0,0 +1,50 @@ +// ignore_for_file: sort_constructors_first + +import 'dart:typed_data'; + +import 'package:web3dart/web3dart.dart'; + +import '../../../crypto.dart'; +import '../../../json_rpc_multiquery.dart'; + +part 'factories.dart'; + +/// D stands for decoded result +/// R stands for raw result +/// The idea is to maintain a stable typing when expecting raw results +/// and when using functions to parsing them. +/// Sadly Dart is not flexible with generic constructors nor factories, +/// so all "factories" are static methods (view factories.dart file) +typedef DecodableFunction = D Function(R); + +class EthRPCQuery extends RPCQuery { + final DecodableFunction _decodeFunction; + + EthRPCQuery._({ + required String function, + List params = const [], + String? id, + required DecodableFunction decodeFn, + }) : _decodeFunction = decodeFn, + super(function, params, id); + + EthQueryResult decodeResult(R rawResult) => + EthQueryResult(_decodeFunction(rawResult), id!); + + EthRPCQuery copyWithId( + String id, + ) => + EthRPCQuery._( + id: id, + function: function, + params: params ?? [], + decodeFn: _decodeFunction, + ); +} + +class EthQueryResult { + EthQueryResult(this.result, this.id); + + final T result; + final String id; +} diff --git a/lib/src/core/eth_rpc_query/factories.dart b/lib/src/core/eth_rpc_query/factories.dart new file mode 100644 index 00000000..b4da0e93 --- /dev/null +++ b/lib/src/core/eth_rpc_query/factories.dart @@ -0,0 +1,258 @@ +part of 'eth_rpc_query.dart'; + +/// Set of useful factories to easily instantiate an EthPRCQuery +extension Factories on EthRPCQuery { + /// Returns balance in Ether wei units of the address. (hex) + static EthRPCQuery getBalance({ + required EthereumAddress address, + BlockNum atBlock = const BlockNum.current(), + String? id, + }) => + EthRPCQuery._( + function: 'eth_getBalance', + params: [ + address.hex, + atBlock.toBlockParam(), + ], + id: id, + decodeFn: (r) => hexToInt(r), + ); + + /// Returns the amount of Ether in wei typically needed to pay for + /// one unit of gas. (hex) + static EthRPCQuery getGasPrice(String? id) => + EthRPCQuery._( + function: 'eth_gasPrice', + id: id, + decodeFn: (r) => EtherAmount.fromHex(r), + ); + + static EthRPCQuery estimateGas( + String? id, + ) => + EthRPCQuery._( + function: 'eth_estimateGas', + id: id, + decodeFn: (r) => EtherAmount.fromHex(r), + ); + + /// Returns the result of calling a contract as a List of returned results + static EthRPCQuery callContract({ + required EthContractCallParams contractCallParams, + BlockNum block = const BlockNum.current(), + String? id, + }) => + EthRPCQuery, String>._( + function: 'eth_call', + params: [ + { + 'to': contractCallParams.contract.address.hex, + 'data': bytesToHex( + contractCallParams.function.encodeCall( + contractCallParams.params, + ), + include0x: true, + padToEvenLength: true, + ), + if (contractCallParams.sender != null) + 'from': contractCallParams.sender!.hex, + }, + block, + ], + id: id, + decodeFn: (r) { + return contractCallParams.function.decodeReturnValues(r); + }, + ); + + /// Returns metadata of a certain block. [returnTransactionObjects] + /// parameter defines if txs details should be returned in this call, + /// or only the tx hashes. (map) + static EthRPCQuery getBlockInformation({ + required BlockNum block, + bool returnTransactionObjects = false, + String? id, + }) => + EthRPCQuery>._( + function: 'eth_getBlockByNumber', + params: [ + block.toBlockParam(), + returnTransactionObjects, + ], + id: id, + decodeFn: (r) => BlockInformation.fromJson(r), + ); + + static EthRPCQuery getTransactionCount({ + required EthereumAddress address, + BlockNum blockNum = const BlockNum.current(), + String? id, + }) => + EthRPCQuery._( + function: 'eth_getTransactionCount', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + static EthRPCQuery sendRawTransaction( + Uint8List signedTransaction, + String? id, + ) => + EthRPCQuery._( + function: 'eth_sendRawTransaction', + params: [ + bytesToHex( + signedTransaction, + include0x: true, + padToEvenLength: true, + ) + ], + id: id, + decodeFn: (r) => r, + ); + + /// Returns the information of a transaction + static EthRPCQuery getTransactionByHash( + String hash, + String? id, + ) => + EthRPCQuery?>._( + function: 'eth_getTransactionByHash', + params: [hash], + id: id, + decodeFn: (r) => r != null ? TransactionInformation.fromMap(r) : null, + ); + + /// Returns a receipt of a transaction + static EthRPCQuery getTransactionReceipt( + String hash, + String? id, + ) => + EthRPCQuery?>._( + function: 'eth_getTransactionReceipt', + params: [hash], + id: id, + decodeFn: (r) => r != null ? TransactionReceipt.fromMap(r) : null, + ); + + // Returns version of the client (String) + static EthRPCQuery getClientVersion(String? id) => + EthRPCQuery._( + function: 'web3_clientVersion', + id: id, + decodeFn: (r) => r, + ); + + /// Returns network id (int) + static EthRPCQuery getNetworkId(String? id) => EthRPCQuery._( + function: 'net_version', + id: id, + decodeFn: (r) => int.parse(r), + ); + + /// Returns chain id (hex) + /// https://chainid.network/chains.json + static EthRPCQuery getChainId(String? id) => EthRPCQuery._( + function: 'eth_chainId', + id: id, + decodeFn: (r) => hexToInt(r), + ); + + /// Returns the version of the Ethereum-protocol (hex) + static EthRPCQuery getEthProtocolVersion(String? id) => + EthRPCQuery._( + function: 'eth_protocolVersion', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + /// Returns the coinbase address (hex) + static EthRPCQuery coinbaseAddress(String? id) => + EthRPCQuery._( + function: 'eth_coinbase', + id: id, + decodeFn: (r) => EthereumAddress.fromHex(r), + ); + + /// Returns if the client is currently mining (bool) + static EthRPCQuery isMining(String? id) => EthRPCQuery._( + function: 'eth_mining', + id: id, + decodeFn: (r) => r, + ); + + /// Returns the amount of hashes per second the connected node is + /// mining with. (int) + static EthRPCQuery getMiningHashrate(String? id) => + EthRPCQuery._( + function: 'eth_hashrate', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + /// Returns the number of the most recent mined block on the chain. + /// (int) + static EthRPCQuery getBlockNumber(String? id) => EthRPCQuery._( + function: 'eth_blockNumber', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + /// Return the code at a specific address (hex) + static EthRPCQuery getCode({ + required EthereumAddress address, + BlockNum block = const BlockNum.current(), + String? id, + }) => + EthRPCQuery._( + function: 'eth_getCode', + params: [ + address.hex, + block.toBlockParam(), + ], + id: id, + decodeFn: (r) => hexToBytes(r), + ); +} + +class EthContractCallParams { + EthContractCallParams({ + this.sender, + required this.contract, + required this.function, + required this.params, + this.atBlock = const BlockNum.current(), + this.rpcId, + }); + + final EthereumAddress? sender; + final DeployedContract contract; + final ContractFunction function; + final List params; + final BlockNum? atBlock; + final String? rpcId; +} + +class EthEstimateGasParams { + EthEstimateGasParams({ + this.sender, + this.to, + this.value, + this.amountOfGas, + this.gasPrice, + this.maxPriorityFeePerGas, + this.maxFeePerGas, + this.data, + this.rpcId, + }); + + final EthereumAddress? sender; + final EthereumAddress? to; + final EtherAmount? value; + final BigInt? amountOfGas; + final EtherAmount? gasPrice; + final EtherAmount? maxPriorityFeePerGas; + final EtherAmount? maxFeePerGas; + final Uint8List? data; + final String? rpcId; +} From 07c292a09f69f5ed605daabc30411a6256c21957 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 13 Jan 2023 04:00:56 +1100 Subject: [PATCH 12/26] finished multiquery client results handling and rpc query id autoadd logic --- lib/src/core/multiquery_client.dart | 47 +++++++++++++++++------------ 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/src/core/multiquery_client.dart b/lib/src/core/multiquery_client.dart index 022de040..b3db2b72 100644 --- a/lib/src/core/multiquery_client.dart +++ b/lib/src/core/multiquery_client.dart @@ -10,40 +10,49 @@ class MultiQueryWeb3Client extends Web3Client { socketConnector: socketConnector, ); + /// Method used to make several contract calls and/or various rpc queries, using only + /// one request to the eth client. + /// The resulting list of responses will a mix of [RPCError] instance/s and/or + /// [EthRPCQuery] instance/s with the returned value Future> multiqueryCall( - List contractCalls, - List rawQuerys, + List queries, ) async { // Each instance of contract call is mapped to an id (the index). // This is intended to later find out how to decode the returned values. - final contractCallsMap = contractCalls.asMap(); - int lastId = contractCallsMap.length; - final contractQueries = contractCallsMap.entries.map( - (e) => e.value.toRPCQuery( - e.key.toString(), - ), - ); - final rawQuerysWithId = rawQuerys.map( - (q) => RPCQuery(q.function, q.params, q.id ?? (lastId++).toString()), - ); + late final allQueriesWithId = queries.every((c) => c.id != null); + late final allQueriesWithNoId = queries.every((c) => c.id == null); + // Queries should be passed: all with id or all without id + if (!allQueriesWithId && !allQueriesWithNoId) { + throw ArgumentError( + 'Some but not all querys have been provided with an RPC id.' + 'You must assign an id to each call or leave all calls without any assigned id'); + } + + final Map preparedQueries = {}; + int lastId = 0; + for (var q in queries) { + final id = q.id ?? (lastId++).toString(); + preparedQueries[id] = q.copyWithId(id); + } - final responses = await (_jsonRpc as MultiQueryRpcService).callMultiQuery([ - ...contractQueries, - ...rawQuerysWithId, - ]); + final responses = await (_jsonRpc as MultiQueryRpcService) + .callMultiQuery(preparedQueries.values.toList()); + // The decoded responses will be either [RPCError] instance/s or + // [EthRPCQuery] instance/s with the returned value final decodedResponses = []; for (final res in responses) { // each response can be either an error or a correct value returned if (res is RPCResponse) { - final function = contractCallsMap[res.id]!.function; - final decodedResult = function.decodeReturnValues(res.result as String); + final correspondingQuery = preparedQueries[res.id]!; + final decodedResult = + correspondingQuery.decodeResult(res.result as String); + decodedResponses.add(decodedResult); } else if (res is RPCError) { decodedResponses.add(res); } } - return decodedResponses; } } From 4ad498eff69eeec57cfda18a820891abb50e204b Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 14 Jan 2023 10:58:23 +1100 Subject: [PATCH 13/26] reorganized factories due to extension not working when trying to use them. switched back rpc query id to be int instead of string, for single type managment purpose: theorically an rpc id can be string too, but for simplicity all ids assigned will be int --- lib/json_rpc_multiquery.dart | 2 +- lib/src/core/eth_rpc_query/eth_rpc_query.dart | 233 +++++++++++++++- lib/src/core/eth_rpc_query/factories.dart | 258 ------------------ .../core/eth_rpc_query/params_classes.dart | 43 +++ lib/src/core/multiquery_client.dart | 7 +- 5 files changed, 272 insertions(+), 271 deletions(-) delete mode 100644 lib/src/core/eth_rpc_query/factories.dart create mode 100644 lib/src/core/eth_rpc_query/params_classes.dart diff --git a/lib/json_rpc_multiquery.dart b/lib/json_rpc_multiquery.dart index fc68b4c7..413cda5b 100644 --- a/lib/json_rpc_multiquery.dart +++ b/lib/json_rpc_multiquery.dart @@ -75,6 +75,6 @@ class RPCQuery { RPCQuery(this.function, [this.params, this.id]); final String function; - final String? id; + final int? id; final List? params; } diff --git a/lib/src/core/eth_rpc_query/eth_rpc_query.dart b/lib/src/core/eth_rpc_query/eth_rpc_query.dart index 5877720e..ecc55a34 100644 --- a/lib/src/core/eth_rpc_query/eth_rpc_query.dart +++ b/lib/src/core/eth_rpc_query/eth_rpc_query.dart @@ -1,3 +1,4 @@ +library eth_rpc_query; // ignore_for_file: sort_constructors_first import 'dart:typed_data'; @@ -7,7 +8,9 @@ import 'package:web3dart/web3dart.dart'; import '../../../crypto.dart'; import '../../../json_rpc_multiquery.dart'; -part 'factories.dart'; +export 'eth_rpc_query.dart'; + +part 'params_classes.dart'; /// D stands for decoded result /// R stands for raw result @@ -17,13 +20,25 @@ part 'factories.dart'; /// so all "factories" are static methods (view factories.dart file) typedef DecodableFunction = D Function(R); +class EthQueryResult { + EthQueryResult(this.result, this.id); + + final T result; + final int id; + + @override + String toString() { + return '{"id": $id , "result": $result}'; + } +} + class EthRPCQuery extends RPCQuery { final DecodableFunction _decodeFunction; EthRPCQuery._({ required String function, List params = const [], - String? id, + int? id, required DecodableFunction decodeFn, }) : _decodeFunction = decodeFn, super(function, params, id); @@ -32,7 +47,7 @@ class EthRPCQuery extends RPCQuery { EthQueryResult(_decodeFunction(rawResult), id!); EthRPCQuery copyWithId( - String id, + int id, ) => EthRPCQuery._( id: id, @@ -40,11 +55,213 @@ class EthRPCQuery extends RPCQuery { params: params ?? [], decodeFn: _decodeFunction, ); -} -class EthQueryResult { - EthQueryResult(this.result, this.id); + /// Returns balance in Ether wei units of the address. (hex) + static EthRPCQuery getBalance({ + required EthereumAddress address, + BlockNum atBlock = const BlockNum.current(), + int? id, + }) => + EthRPCQuery._( + function: 'eth_getBalance', + params: [ + address.hex, + atBlock.toBlockParam(), + ], + id: id, + decodeFn: (r) => hexToInt(r), + ); - final T result; - final String id; + /// Returns the amount of Ether in wei typically needed to pay for + /// one unit of gas. (hex) + static EthRPCQuery getGasPrice(int? id) => EthRPCQuery._( + function: 'eth_gasPrice', + id: id, + decodeFn: (r) => EtherAmount.fromHex(r), + ); + + static EthRPCQuery estimateGas( + int? id, + ) => + EthRPCQuery._( + function: 'eth_estimateGas', + id: id, + decodeFn: (r) => EtherAmount.fromHex(r), + ); + + /// Returns the result of calling a contract as a List of returned results + static EthRPCQuery callContract({ + required EthContractCallParams contractCallParams, + BlockNum block = const BlockNum.current(), + int? id, + }) => + EthRPCQuery, String>._( + function: 'eth_call', + params: [ + { + 'to': contractCallParams.contract.address.hex, + 'data': bytesToHex( + contractCallParams.function.encodeCall( + contractCallParams.params, + ), + include0x: true, + padToEvenLength: true, + ), + if (contractCallParams.sender != null) + 'from': contractCallParams.sender!.hex, + }, + block.toBlockParam(), + ], + id: id, + decodeFn: (r) { + return contractCallParams.function.decodeReturnValues(r); + }, + ); + + /// Returns metadata of a certain block. [returnTransactionObjects] + /// parameter defines if txs details should be returned in this call, + /// or only the tx hashes. (map) + static EthRPCQuery getBlockInformation({ + required BlockNum block, + bool returnTransactionObjects = false, + int? id, + }) => + EthRPCQuery>._( + function: 'eth_getBlockByNumber', + params: [ + block.toBlockParam(), + returnTransactionObjects, + ], + id: id, + decodeFn: (r) => BlockInformation.fromJson(r), + ); + + static EthRPCQuery getTransactionCount({ + required EthereumAddress address, + BlockNum blockNum = const BlockNum.current(), + int? id, + }) => + EthRPCQuery._( + function: 'eth_getTransactionCount', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + static EthRPCQuery sendRawTransaction( + Uint8List signedTransaction, + int? id, + ) => + EthRPCQuery._( + function: 'eth_sendRawTransaction', + params: [ + bytesToHex( + signedTransaction, + include0x: true, + padToEvenLength: true, + ) + ], + id: id, + decodeFn: (r) => r, + ); + + /// Returns the information of a transaction + static EthRPCQuery getTransactionByHash( + String hash, + int? id, + ) => + EthRPCQuery?>._( + function: 'eth_getTransactionByHash', + params: [hash], + id: id, + decodeFn: (r) => r != null ? TransactionInformation.fromMap(r) : null, + ); + + /// Returns a receipt of a transaction + static EthRPCQuery getTransactionReceipt( + String hash, + int? id, + ) => + EthRPCQuery?>._( + function: 'eth_getTransactionReceipt', + params: [hash], + id: id, + decodeFn: (r) => r != null ? TransactionReceipt.fromMap(r) : null, + ); + + // Returns version of the client (String) + static EthRPCQuery getClientVersion(int? id) => EthRPCQuery._( + function: 'web3_clientVersion', + id: id, + decodeFn: (r) => r, + ); + + /// Returns network id (int) + static EthRPCQuery getNetworkId(int? id) => EthRPCQuery._( + function: 'net_version', + id: id, + decodeFn: (r) => int.parse(r), + ); + + /// Returns chain id (hex) + /// https://chainid.network/chains.json + static EthRPCQuery getChainId(int? id) => EthRPCQuery._( + function: 'eth_chainId', + id: id, + decodeFn: (r) => hexToInt(r), + ); + + /// Returns the version of the Ethereum-protocol (hex) + static EthRPCQuery getEthProtocolVersion(int? id) => + EthRPCQuery._( + function: 'eth_protocolVersion', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + /// Returns the coinbase address (hex) + static EthRPCQuery coinbaseAddress(int? id) => + EthRPCQuery._( + function: 'eth_coinbase', + id: id, + decodeFn: (r) => EthereumAddress.fromHex(r), + ); + + /// Returns if the client is currently mining (bool) + static EthRPCQuery isMining(int? id) => EthRPCQuery._( + function: 'eth_mining', + id: id, + decodeFn: (r) => r, + ); + + /// Returns the amount of hashes per second the connected node is + /// mining with. (int) + static EthRPCQuery getMiningHashrate(int? id) => EthRPCQuery._( + function: 'eth_hashrate', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + /// Returns the number of the most recent mined block on the chain. + /// (int) + static EthRPCQuery getBlockNumber(int? id) => EthRPCQuery._( + function: 'eth_blockNumber', + id: id, + decodeFn: (r) => hexToDartInt(r), + ); + + /// Return the code at a specific address (hex) + static EthRPCQuery getCode({ + required EthereumAddress address, + BlockNum block = const BlockNum.current(), + int? id, + }) => + EthRPCQuery._( + function: 'eth_getCode', + params: [ + address.hex, + block.toBlockParam(), + ], + id: id, + decodeFn: (r) => hexToBytes(r), + ); } diff --git a/lib/src/core/eth_rpc_query/factories.dart b/lib/src/core/eth_rpc_query/factories.dart deleted file mode 100644 index b4da0e93..00000000 --- a/lib/src/core/eth_rpc_query/factories.dart +++ /dev/null @@ -1,258 +0,0 @@ -part of 'eth_rpc_query.dart'; - -/// Set of useful factories to easily instantiate an EthPRCQuery -extension Factories on EthRPCQuery { - /// Returns balance in Ether wei units of the address. (hex) - static EthRPCQuery getBalance({ - required EthereumAddress address, - BlockNum atBlock = const BlockNum.current(), - String? id, - }) => - EthRPCQuery._( - function: 'eth_getBalance', - params: [ - address.hex, - atBlock.toBlockParam(), - ], - id: id, - decodeFn: (r) => hexToInt(r), - ); - - /// Returns the amount of Ether in wei typically needed to pay for - /// one unit of gas. (hex) - static EthRPCQuery getGasPrice(String? id) => - EthRPCQuery._( - function: 'eth_gasPrice', - id: id, - decodeFn: (r) => EtherAmount.fromHex(r), - ); - - static EthRPCQuery estimateGas( - String? id, - ) => - EthRPCQuery._( - function: 'eth_estimateGas', - id: id, - decodeFn: (r) => EtherAmount.fromHex(r), - ); - - /// Returns the result of calling a contract as a List of returned results - static EthRPCQuery callContract({ - required EthContractCallParams contractCallParams, - BlockNum block = const BlockNum.current(), - String? id, - }) => - EthRPCQuery, String>._( - function: 'eth_call', - params: [ - { - 'to': contractCallParams.contract.address.hex, - 'data': bytesToHex( - contractCallParams.function.encodeCall( - contractCallParams.params, - ), - include0x: true, - padToEvenLength: true, - ), - if (contractCallParams.sender != null) - 'from': contractCallParams.sender!.hex, - }, - block, - ], - id: id, - decodeFn: (r) { - return contractCallParams.function.decodeReturnValues(r); - }, - ); - - /// Returns metadata of a certain block. [returnTransactionObjects] - /// parameter defines if txs details should be returned in this call, - /// or only the tx hashes. (map) - static EthRPCQuery getBlockInformation({ - required BlockNum block, - bool returnTransactionObjects = false, - String? id, - }) => - EthRPCQuery>._( - function: 'eth_getBlockByNumber', - params: [ - block.toBlockParam(), - returnTransactionObjects, - ], - id: id, - decodeFn: (r) => BlockInformation.fromJson(r), - ); - - static EthRPCQuery getTransactionCount({ - required EthereumAddress address, - BlockNum blockNum = const BlockNum.current(), - String? id, - }) => - EthRPCQuery._( - function: 'eth_getTransactionCount', - id: id, - decodeFn: (r) => hexToDartInt(r), - ); - - static EthRPCQuery sendRawTransaction( - Uint8List signedTransaction, - String? id, - ) => - EthRPCQuery._( - function: 'eth_sendRawTransaction', - params: [ - bytesToHex( - signedTransaction, - include0x: true, - padToEvenLength: true, - ) - ], - id: id, - decodeFn: (r) => r, - ); - - /// Returns the information of a transaction - static EthRPCQuery getTransactionByHash( - String hash, - String? id, - ) => - EthRPCQuery?>._( - function: 'eth_getTransactionByHash', - params: [hash], - id: id, - decodeFn: (r) => r != null ? TransactionInformation.fromMap(r) : null, - ); - - /// Returns a receipt of a transaction - static EthRPCQuery getTransactionReceipt( - String hash, - String? id, - ) => - EthRPCQuery?>._( - function: 'eth_getTransactionReceipt', - params: [hash], - id: id, - decodeFn: (r) => r != null ? TransactionReceipt.fromMap(r) : null, - ); - - // Returns version of the client (String) - static EthRPCQuery getClientVersion(String? id) => - EthRPCQuery._( - function: 'web3_clientVersion', - id: id, - decodeFn: (r) => r, - ); - - /// Returns network id (int) - static EthRPCQuery getNetworkId(String? id) => EthRPCQuery._( - function: 'net_version', - id: id, - decodeFn: (r) => int.parse(r), - ); - - /// Returns chain id (hex) - /// https://chainid.network/chains.json - static EthRPCQuery getChainId(String? id) => EthRPCQuery._( - function: 'eth_chainId', - id: id, - decodeFn: (r) => hexToInt(r), - ); - - /// Returns the version of the Ethereum-protocol (hex) - static EthRPCQuery getEthProtocolVersion(String? id) => - EthRPCQuery._( - function: 'eth_protocolVersion', - id: id, - decodeFn: (r) => hexToDartInt(r), - ); - - /// Returns the coinbase address (hex) - static EthRPCQuery coinbaseAddress(String? id) => - EthRPCQuery._( - function: 'eth_coinbase', - id: id, - decodeFn: (r) => EthereumAddress.fromHex(r), - ); - - /// Returns if the client is currently mining (bool) - static EthRPCQuery isMining(String? id) => EthRPCQuery._( - function: 'eth_mining', - id: id, - decodeFn: (r) => r, - ); - - /// Returns the amount of hashes per second the connected node is - /// mining with. (int) - static EthRPCQuery getMiningHashrate(String? id) => - EthRPCQuery._( - function: 'eth_hashrate', - id: id, - decodeFn: (r) => hexToDartInt(r), - ); - - /// Returns the number of the most recent mined block on the chain. - /// (int) - static EthRPCQuery getBlockNumber(String? id) => EthRPCQuery._( - function: 'eth_blockNumber', - id: id, - decodeFn: (r) => hexToDartInt(r), - ); - - /// Return the code at a specific address (hex) - static EthRPCQuery getCode({ - required EthereumAddress address, - BlockNum block = const BlockNum.current(), - String? id, - }) => - EthRPCQuery._( - function: 'eth_getCode', - params: [ - address.hex, - block.toBlockParam(), - ], - id: id, - decodeFn: (r) => hexToBytes(r), - ); -} - -class EthContractCallParams { - EthContractCallParams({ - this.sender, - required this.contract, - required this.function, - required this.params, - this.atBlock = const BlockNum.current(), - this.rpcId, - }); - - final EthereumAddress? sender; - final DeployedContract contract; - final ContractFunction function; - final List params; - final BlockNum? atBlock; - final String? rpcId; -} - -class EthEstimateGasParams { - EthEstimateGasParams({ - this.sender, - this.to, - this.value, - this.amountOfGas, - this.gasPrice, - this.maxPriorityFeePerGas, - this.maxFeePerGas, - this.data, - this.rpcId, - }); - - final EthereumAddress? sender; - final EthereumAddress? to; - final EtherAmount? value; - final BigInt? amountOfGas; - final EtherAmount? gasPrice; - final EtherAmount? maxPriorityFeePerGas; - final EtherAmount? maxFeePerGas; - final Uint8List? data; - final String? rpcId; -} diff --git a/lib/src/core/eth_rpc_query/params_classes.dart b/lib/src/core/eth_rpc_query/params_classes.dart new file mode 100644 index 00000000..c0f26c14 --- /dev/null +++ b/lib/src/core/eth_rpc_query/params_classes.dart @@ -0,0 +1,43 @@ +part of 'eth_rpc_query.dart'; + +class EthContractCallParams { + EthContractCallParams({ + this.sender, + required this.contract, + required this.function, + required this.params, + this.atBlock = const BlockNum.current(), + this.rpcId, + }); + + final EthereumAddress? sender; + final DeployedContract contract; + final ContractFunction function; + final List params; + final BlockNum? atBlock; + final String? rpcId; +} + +class EthEstimateGasParams { + EthEstimateGasParams({ + this.sender, + this.to, + this.value, + this.amountOfGas, + this.gasPrice, + this.maxPriorityFeePerGas, + this.maxFeePerGas, + this.data, + this.rpcId, + }); + + final EthereumAddress? sender; + final EthereumAddress? to; + final EtherAmount? value; + final BigInt? amountOfGas; + final EtherAmount? gasPrice; + final EtherAmount? maxPriorityFeePerGas; + final EtherAmount? maxFeePerGas; + final Uint8List? data; + final String? rpcId; +} diff --git a/lib/src/core/multiquery_client.dart b/lib/src/core/multiquery_client.dart index b3db2b72..a9099263 100644 --- a/lib/src/core/multiquery_client.dart +++ b/lib/src/core/multiquery_client.dart @@ -29,10 +29,10 @@ class MultiQueryWeb3Client extends Web3Client { 'You must assign an id to each call or leave all calls without any assigned id'); } - final Map preparedQueries = {}; + final Map preparedQueries = {}; int lastId = 0; for (var q in queries) { - final id = q.id ?? (lastId++).toString(); + final id = q.id ?? lastId++; preparedQueries[id] = q.copyWithId(id); } @@ -45,8 +45,7 @@ class MultiQueryWeb3Client extends Web3Client { // each response can be either an error or a correct value returned if (res is RPCResponse) { final correspondingQuery = preparedQueries[res.id]!; - final decodedResult = - correspondingQuery.decodeResult(res.result as String); + final decodedResult = correspondingQuery.decodeResult(res.result); decodedResponses.add(decodedResult); } else if (res is RPCError) { From 11d568775d922aed51b3272a7954b8d169e467ff Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Sat, 14 Jan 2023 10:58:54 +1100 Subject: [PATCH 14/26] added integration tests to multiquery client --- test/multiquery_client_integration_test.dart | 176 +++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 test/multiquery_client_integration_test.dart diff --git a/test/multiquery_client_integration_test.dart b/test/multiquery_client_integration_test.dart new file mode 100644 index 00000000..0c12ae9b --- /dev/null +++ b/test/multiquery_client_integration_test.dart @@ -0,0 +1,176 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:test/test.dart'; +import 'package:web3dart/src/core/eth_rpc_query/eth_rpc_query.dart'; +import 'package:web3dart/web3dart.dart'; + +const infuraProjectId = String.fromEnvironment('INFURA_ID'); + +void main() { + final contract = DeployedContract( + ContractAbi.fromJson(erc20TestTokenAbi, 'Link ERC20'), + EthereumAddress.fromHex( + '0x326C977E6efc84E512bB9C30f76E30c160eD06FB', + ), + ); + group('integration', () { + late final MultiQueryWeb3Client client; + + setUpAll(() { + client = MultiQueryWeb3Client( + // public rpc https://chainlist.org/chain/5 + 'https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161', + Client(), + ); + }); + + // ignore: unnecessary_lambdas, https://github.com/dart-lang/linter/issues/2670 + tearDownAll(() => client.dispose()); + + test('Multiquery request success', () async { + final queries = [ + EthRPCQuery.getBalance( + address: EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + ), + EthRPCQuery.callContract( + contractCallParams: EthContractCallParams( + contract: contract, + function: contract.function('balanceOf'), + params: [ + EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + ], + ), + ), + EthRPCQuery.getBlockInformation(block: BlockNum.exact(8302276)) + ]; + + final responses = await client.multiqueryCall(queries); + + expect(responses, everyElement(isA())); + + final balanceResult = (responses[0] as EthQueryResult); + expect( + balanceResult.id, + equals(queries[0].id), + ); + expect(balanceResult.result, greaterThan(BigInt.zero)); + + final erc20BalanceResult = (responses[1] as EthQueryResult); + expect( + erc20BalanceResult.id, + equals(queries[1].id), + ); + // contract result azlways come in a list + expect(erc20BalanceResult.result[0], greaterThan(BigInt.zero)); + + final blockInfoResult = (responses[2] as EthQueryResult); + expect( + blockInfoResult.id, + equals(queries[2].id), + ); + expect(blockInfoResult.result, isA()); + }); + }); + + group( + 'query id assignment', + () { + late final MultiQueryWeb3Client client; + + setUpAll(() { + client = MultiQueryWeb3Client( + 'mock url', + MockClient(), + ); + }); + test('Multiquery request arguments - ids assigned OK', () async { + final queries = [ + EthRPCQuery.getBalance( + address: EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + id: 2, + ), + EthRPCQuery.getBalance( + address: EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + id: 1, + ), + ]; + + expect( + () { + client.multiqueryCall(queries); + }, + returnsNormally, + ); + }); + + test('Multiquery request arguments - failure, bad rpc ids assignment', + () async { + final queries = [ + EthRPCQuery.getBalance( + address: EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + id: 4, + ), + EthRPCQuery.callContract( + contractCallParams: EthContractCallParams( + contract: contract, + function: contract.function('balanceOf'), + params: [ + EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + ], + ), + id: 1, + ), + EthRPCQuery.getBlockInformation( + block: BlockNum.exact(8302276), + // here we avoid specifying an id to make it throw + ) + ]; + + expect( + client.multiqueryCall(queries), + throwsArgumentError, + reason: + 'As a bad assignment in querys id, calling this method should throw', + ); + }); + }, + ); +} + +class MockClient extends BaseClient { + StreamedResponse? nextResponse; + BaseRequest? request; + + @override + Future send(BaseRequest request) { + this.request = request; + return Future.value( + nextResponse ?? + StreamedResponse( + Stream.value( + utf8.encode( + '[{"id": 1, "jsonrpc": "2.0", "result": "0x1"},' + '{"id": 2, "jsonrpc": "2.0", "result": "0x1"}]', + ), + ), + 200, + ), + ); + } +} + +const erc20TestTokenAbi = + '[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"},{"name":"_data","type":"bytes"}],"name":"transferAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseApproval","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseApproval","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"data","type":"bytes"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]'; From d1d2fa96dce8b29a56df36d4aa60fa6c11e09ae7 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 27 Feb 2023 00:45:14 +1000 Subject: [PATCH 15/26] start request json rpc id at 0 instead of 1 --- lib/json_rpc_multiquery.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/json_rpc_multiquery.dart b/lib/json_rpc_multiquery.dart index 413cda5b..5eadca39 100644 --- a/lib/json_rpc_multiquery.dart +++ b/lib/json_rpc_multiquery.dart @@ -24,7 +24,7 @@ abstract class MultiQueryRpcService { class JsonRPCMultiQuery extends JsonRPC implements MultiQueryRpcService { JsonRPCMultiQuery(String url, Client client) : super(url, client); - int _currentRequestId = 1; + int _currentRequestId = 0; @override Future> callMultiQuery(List queries) async { From d19217e9d44837d320f3554a174816f82128010a Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 27 Feb 2023 00:46:02 +1000 Subject: [PATCH 16/26] added multiquery client as a library export --- lib/web3dart.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/web3dart.dart b/lib/web3dart.dart index 552155fd..923c3602 100644 --- a/lib/web3dart.dart +++ b/lib/web3dart.dart @@ -30,6 +30,7 @@ export 'src/core/amount.dart'; export 'src/core/block_information.dart'; export 'src/core/block_number.dart'; export 'src/core/sync_information.dart'; +export 'src/core/eth_rpc_query/eth_rpc_query.dart'; part 'src/core/multiquery_client.dart'; part 'src/core/client.dart'; From 63b57481cc94eeb6c42e35c5c9926f6fede6ad2b Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 27 Feb 2023 00:47:04 +1000 Subject: [PATCH 17/26] added error throwing when eth client doesnt respond with expected number of responses added response sorted by request order (not id order) --- lib/src/core/multiquery_client.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/src/core/multiquery_client.dart b/lib/src/core/multiquery_client.dart index a9099263..0b30eb7b 100644 --- a/lib/src/core/multiquery_client.dart +++ b/lib/src/core/multiquery_client.dart @@ -38,6 +38,12 @@ class MultiQueryWeb3Client extends Web3Client { final responses = await (_jsonRpc as MultiQueryRpcService) .callMultiQuery(preparedQueries.values.toList()); + if (responses.length != queries.length) { + throw Error.throwWithStackTrace( + 'Eth node client did not respond correctly to all the queries', + StackTrace.current, + ); + } // The decoded responses will be either [RPCError] instance/s or // [EthRPCQuery] instance/s with the returned value final decodedResponses = []; @@ -52,6 +58,22 @@ class MultiQueryWeb3Client extends Web3Client { decodedResponses.add(res); } } + + // sorting responses by querys order (not id order) + final sortedResponsesList = []; + + for (var k = 0; k > preparedQueries.keys.length; k++) { + final sameIdResponse = decodedResponses.firstWhere((dynamic r) { + if (r is RPCError) { + return r.id == preparedQueries[k]!.id; + } else if (r is RPCResponse) { + return r.id == preparedQueries[k]!.id; + } + return false; + }); + sortedResponsesList[k] = sameIdResponse; + } + return decodedResponses; } } From 01dfe3ca2201b4a69a57308447f5605e817e7458 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 27 Feb 2023 00:47:31 +1000 Subject: [PATCH 18/26] updated integration test --- test/multiquery_client_integration_test.dart | 36 ++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/test/multiquery_client_integration_test.dart b/test/multiquery_client_integration_test.dart index 0c12ae9b..f523f076 100644 --- a/test/multiquery_client_integration_test.dart +++ b/test/multiquery_client_integration_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:test/test.dart'; -import 'package:web3dart/src/core/eth_rpc_query/eth_rpc_query.dart'; import 'package:web3dart/web3dart.dart'; const infuraProjectId = String.fromEnvironment('INFURA_ID'); @@ -31,11 +30,13 @@ void main() { test('Multiquery request success', () async { final queries = [ EthRPCQuery.getBalance( + id: 2, address: EthereumAddress.fromHex( '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', ), ), EthRPCQuery.callContract( + id: 1, contractCallParams: EthContractCallParams( contract: contract, function: contract.function('balanceOf'), @@ -46,7 +47,10 @@ void main() { ], ), ), - EthRPCQuery.getBlockInformation(block: BlockNum.exact(8302276)) + EthRPCQuery.getBlockInformation( + block: BlockNum.exact(8302276), + id: 3, + ) ]; final responses = await client.multiqueryCall(queries); @@ -78,7 +82,7 @@ void main() { }); group( - 'query id assignment', + 'Query id assignment', () { late final MultiQueryWeb3Client client; @@ -94,7 +98,7 @@ void main() { address: EthereumAddress.fromHex( '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', ), - id: 2, + id: 0, ), EthRPCQuery.getBalance( address: EthereumAddress.fromHex( @@ -111,7 +115,27 @@ void main() { returnsNormally, ); }); + test('Multiquery request arguments - no ids assigned', () async { + final queries = [ + EthRPCQuery.getBalance( + address: EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + ), + EthRPCQuery.getBalance( + address: EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + ), + ]; + expect( + () { + client.multiqueryCall(queries); + }, + returnsNormally, + ); + }); test('Multiquery request arguments - failure, bad rpc ids assignment', () async { final queries = [ @@ -162,8 +186,8 @@ class MockClient extends BaseClient { StreamedResponse( Stream.value( utf8.encode( - '[{"id": 1, "jsonrpc": "2.0", "result": "0x1"},' - '{"id": 2, "jsonrpc": "2.0", "result": "0x1"}]', + '[{"id": "0", "jsonrpc": "2.0", "result": "0x1"},' + '{"id": "1", "jsonrpc": "2.0", "result": "0x1"}]', ), ), 200, From 3f8d66075858cf737a08fb2060fbf43ed9289921 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 27 Feb 2023 22:30:33 +1000 Subject: [PATCH 19/26] fix camel case name --- lib/src/core/multiquery_client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/core/multiquery_client.dart b/lib/src/core/multiquery_client.dart index 0b30eb7b..826e1ec7 100644 --- a/lib/src/core/multiquery_client.dart +++ b/lib/src/core/multiquery_client.dart @@ -14,7 +14,7 @@ class MultiQueryWeb3Client extends Web3Client { /// one request to the eth client. /// The resulting list of responses will a mix of [RPCError] instance/s and/or /// [EthRPCQuery] instance/s with the returned value - Future> multiqueryCall( + Future> multiQueryCall( List queries, ) async { // Each instance of contract call is mapped to an id (the index). From 7d79f16d2423d23224a8e4d6df9da92d136e9004 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 27 Feb 2023 22:31:30 +1000 Subject: [PATCH 20/26] fixed import/export directives for part and part of directives --- lib/json_rpc_multiquery.dart | 10 +--------- lib/src/core/eth_rpc_query/eth_rpc_query.dart | 18 +++--------------- lib/src/core/eth_rpc_query/params_classes.dart | 2 +- lib/web3dart.dart | 7 ++++--- 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/lib/json_rpc_multiquery.dart b/lib/json_rpc_multiquery.dart index 5eadca39..63540e31 100644 --- a/lib/json_rpc_multiquery.dart +++ b/lib/json_rpc_multiquery.dart @@ -1,12 +1,4 @@ -// ignore_for_file: sort_constructors_first - -library json_rpc_multiquery; - -import 'dart:convert'; - -import 'package:http/http.dart'; - -import 'json_rpc.dart'; +part of web3dart; abstract class MultiQueryRpcService { /// Performs a single RPC request, asking the server to execute several queries diff --git a/lib/src/core/eth_rpc_query/eth_rpc_query.dart b/lib/src/core/eth_rpc_query/eth_rpc_query.dart index ecc55a34..0749eadc 100644 --- a/lib/src/core/eth_rpc_query/eth_rpc_query.dart +++ b/lib/src/core/eth_rpc_query/eth_rpc_query.dart @@ -1,16 +1,4 @@ -library eth_rpc_query; -// ignore_for_file: sort_constructors_first - -import 'dart:typed_data'; - -import 'package:web3dart/web3dart.dart'; - -import '../../../crypto.dart'; -import '../../../json_rpc_multiquery.dart'; - -export 'eth_rpc_query.dart'; - -part 'params_classes.dart'; +part of web3dart; /// D stands for decoded result /// R stands for raw result @@ -33,8 +21,6 @@ class EthQueryResult { } class EthRPCQuery extends RPCQuery { - final DecodableFunction _decodeFunction; - EthRPCQuery._({ required String function, List params = const [], @@ -43,6 +29,8 @@ class EthRPCQuery extends RPCQuery { }) : _decodeFunction = decodeFn, super(function, params, id); + final DecodableFunction _decodeFunction; + EthQueryResult decodeResult(R rawResult) => EthQueryResult(_decodeFunction(rawResult), id!); diff --git a/lib/src/core/eth_rpc_query/params_classes.dart b/lib/src/core/eth_rpc_query/params_classes.dart index c0f26c14..27d0f1eb 100644 --- a/lib/src/core/eth_rpc_query/params_classes.dart +++ b/lib/src/core/eth_rpc_query/params_classes.dart @@ -1,4 +1,4 @@ -part of 'eth_rpc_query.dart'; +part of web3dart; class EthContractCallParams { EthContractCallParams({ diff --git a/lib/web3dart.dart b/lib/web3dart.dart index 923c3602..882437ad 100644 --- a/lib/web3dart.dart +++ b/lib/web3dart.dart @@ -1,6 +1,7 @@ library web3dart; import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import 'package:web3dart/src/utils/equality.dart' as eq; @@ -14,11 +15,9 @@ import 'contracts.dart'; import 'credentials.dart'; import 'crypto.dart'; import 'json_rpc.dart'; -import 'json_rpc_multiquery.dart'; import 'src/core/amount.dart'; import 'src/core/block_information.dart'; import 'src/core/block_number.dart'; -import 'src/core/eth_rpc_query/eth_rpc_query.dart'; import 'src/core/sync_information.dart'; import 'src/utils/rlp.dart' as rlp; import 'src/utils/typed_data.dart'; @@ -30,8 +29,10 @@ export 'src/core/amount.dart'; export 'src/core/block_information.dart'; export 'src/core/block_number.dart'; export 'src/core/sync_information.dart'; -export 'src/core/eth_rpc_query/eth_rpc_query.dart'; +part 'src/core/eth_rpc_query/eth_rpc_query.dart'; +part 'src/core/eth_rpc_query/params_classes.dart'; +part 'json_rpc_multiquery.dart'; part 'src/core/multiquery_client.dart'; part 'src/core/client.dart'; part 'src/core/filters.dart'; From 0677d5cdcd3af19e4e2479caa86f6dad7fcb0639 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Mon, 27 Feb 2023 22:31:51 +1000 Subject: [PATCH 21/26] fixed import in test --- test/json_rpc_multiquery_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/json_rpc_multiquery_test.dart b/test/json_rpc_multiquery_test.dart index a3973927..222383bb 100644 --- a/test/json_rpc_multiquery_test.dart +++ b/test/json_rpc_multiquery_test.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:test/test.dart'; import 'package:web3dart/json_rpc.dart'; -import 'package:web3dart/json_rpc_multiquery.dart'; +import 'package:web3dart/web3dart.dart'; void main() { late MockClient client; From 867c8f518eca6f38948e0ff4acd7a6583d8aed33 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Tue, 28 Feb 2023 11:08:00 +1000 Subject: [PATCH 22/26] added capacity for parsing amount in hex from different units and not only from wei --- lib/src/core/amount.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/core/amount.dart b/lib/src/core/amount.dart index 091336aa..30b862c5 100644 --- a/lib/src/core/amount.dart +++ b/lib/src/core/amount.dart @@ -47,10 +47,12 @@ class EtherAmount { return EtherAmount.inWei(parsedAmount * _factors[unit]!); } - /// Constructs an amount of Ether in wei from an hex String - factory EtherAmount.fromHex(String hex) { + /// Constructs an amount of Ether from an hex String. + /// Most of the times, the amount comes express in wei, but any unit cna be + /// used. + factory EtherAmount.fromHex(String hex, [EtherUnit unit = EtherUnit.wei]) { final amountInBigInt = hexToInt(hex); - return EtherAmount.inWei(amountInBigInt); + return EtherAmount.fromUnitAndValue(unit, amountInBigInt); } /// Gets the value of this amount in the specified unit as a whole number. From 2c1d5b1a9faaac0b916a885f7084b755b09056a6 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Thu, 2 Mar 2023 23:31:45 +1000 Subject: [PATCH 23/26] fix rename typos --- test/multiquery_client_integration_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/multiquery_client_integration_test.dart b/test/multiquery_client_integration_test.dart index f523f076..2618e6cd 100644 --- a/test/multiquery_client_integration_test.dart +++ b/test/multiquery_client_integration_test.dart @@ -53,7 +53,7 @@ void main() { ) ]; - final responses = await client.multiqueryCall(queries); + final responses = await client.multiQueryCall(queries); expect(responses, everyElement(isA())); @@ -110,7 +110,7 @@ void main() { expect( () { - client.multiqueryCall(queries); + client.multiQueryCall(queries); }, returnsNormally, ); @@ -131,7 +131,7 @@ void main() { expect( () { - client.multiqueryCall(queries); + client.multiQueryCall(queries); }, returnsNormally, ); @@ -164,7 +164,7 @@ void main() { ]; expect( - client.multiqueryCall(queries), + client.multiQueryCall(queries), throwsArgumentError, reason: 'As a bad assignment in querys id, calling this method should throw', From 5fc330a443b3d4d50cc37a7a2fc6932d0c9d14dd Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Thu, 2 Mar 2023 23:36:25 +1000 Subject: [PATCH 24/26] added documentation for muliquery request --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index 63c32abf..4a409225 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,54 @@ obtained from the connected node when not explicitly specified. If you only need the signed transaction but don't intend to send it, you can use `client.signTransaction`. +## Request with multiple querys + +As of JSON-RPC specification, one can make several queries in one http request. +For using this feature, instead of using the common Web3Client, you have to instantiate +a `MultiQueryWeb3Client`, which has the same functionality as ao Web3Client, but adds the +`client.multiQueryCall` method. +For usability purposes, some helper classes have been implemented to ask for different required queries inside the same request. + +The `multiqueryCall` method requires a list of `ETHRpcQuery` instances +You can easily construct them thanks to these useful custom constructors. For instance, to ask for the balance, you can use `ETHRpcQuery.getBalance`. + +All queries inside this list must satisfy a condition: either all of them or none of them should have an rpc query id assigned (to manage the responses id from the server). + +For any other not specified rpc method, you can construct it yourself using the normal `ETHRpcQuery` constructor. The trick here would be correctly parsing the result as desired with the `decodeFn` parameter. + +Example usage: + +```dart +final client = MultiQueryWeb3Client(apiUrl, Client()); +final queries = [ + EthRPCQuery.getBalance( + id: 2, + address: EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + ), + EthRPCQuery.callContract( + id: 1, + contractCallParams: EthContractCallParams( + contract: contract, + function: contract.function('balanceOf'), + params: [ + EthereumAddress.fromHex( + '0x81bEdCC7314baf7606b665909CeCDB4c68b180d6', + ), + ], + ), + ), + EthRPCQuery.getBlockInformation( + block: BlockNum.exact(8302276), + id: 3, + ) + ]; + + final responses = await client.multiQueryCall(queries); +``` + + ### Smart contracts The library can parse the abi of a smart contract and send data to it. It can also From 504ad7e5d9f2e8a1f4c10e27328d9c84aed2d937 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Thu, 3 Apr 2025 23:40:26 +0330 Subject: [PATCH 25/26] Format code. --- lib/json_rpc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/json_rpc.dart b/lib/json_rpc.dart index ce0691be..99df0ba8 100644 --- a/lib/json_rpc.dart +++ b/lib/json_rpc.dart @@ -95,7 +95,7 @@ class RPCError implements Exception { /// Constructor. const RPCError(this.errorCode, this.message, this.data, [this.id]); -/// Id. + /// Id. final int? id; /// Error code. From 4df380ded97d2b2a720c93fe0170000897a1bf36 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Thu, 3 Apr 2025 23:52:13 +0330 Subject: [PATCH 26/26] Cleanup. --- lib/src/core/client.dart | 2 +- lib/src/core/eth_rpc_query/eth_rpc_query.dart | 2 +- lib/src/core/eth_rpc_query/params_classes.dart | 2 +- lib/src/core/filters.dart | 2 +- lib/src/core/multiquery_client.dart | 2 +- lib/src/core/transaction.dart | 2 +- lib/src/core/transaction_information.dart | 2 +- lib/{ => src/rpc}/json_rpc.dart | 9 +-------- lib/{ => src/rpc}/json_rpc_multiquery.dart | 2 +- lib/src/utils/length_tracking_byte_sink.dart | 2 +- lib/web3dart.dart | 6 ++++-- test/json_rpc_multiquery_test.dart | 1 - test/json_rpc_test.dart | 2 +- 13 files changed, 15 insertions(+), 21 deletions(-) rename lib/{ => src/rpc}/json_rpc.dart (95%) rename lib/{ => src/rpc}/json_rpc_multiquery.dart (98%) diff --git a/lib/src/core/client.dart b/lib/src/core/client.dart index e0a71e56..81c0525b 100644 --- a/lib/src/core/client.dart +++ b/lib/src/core/client.dart @@ -1,4 +1,4 @@ -part of 'package:web3dart/web3dart.dart'; +part of '../../web3dart.dart'; /// Signature for a function that opens a socket on which json-rpc operations /// can be performed. diff --git a/lib/src/core/eth_rpc_query/eth_rpc_query.dart b/lib/src/core/eth_rpc_query/eth_rpc_query.dart index 6f75a700..05b92657 100644 --- a/lib/src/core/eth_rpc_query/eth_rpc_query.dart +++ b/lib/src/core/eth_rpc_query/eth_rpc_query.dart @@ -1,4 +1,4 @@ -part of web3dart; +part of '../../../web3dart.dart'; /// D stands for decoded result /// R stands for raw result diff --git a/lib/src/core/eth_rpc_query/params_classes.dart b/lib/src/core/eth_rpc_query/params_classes.dart index 27d0f1eb..1ad0f6a8 100644 --- a/lib/src/core/eth_rpc_query/params_classes.dart +++ b/lib/src/core/eth_rpc_query/params_classes.dart @@ -1,4 +1,4 @@ -part of web3dart; +part of '../../../web3dart.dart'; class EthContractCallParams { EthContractCallParams({ diff --git a/lib/src/core/filters.dart b/lib/src/core/filters.dart index a97dcec0..8786a4dd 100644 --- a/lib/src/core/filters.dart +++ b/lib/src/core/filters.dart @@ -1,4 +1,4 @@ -part of 'package:web3dart/web3dart.dart'; +part of '../../web3dart.dart'; class _FilterCreationParams { _FilterCreationParams(this.method, this.params); diff --git a/lib/src/core/multiquery_client.dart b/lib/src/core/multiquery_client.dart index 826e1ec7..60a2d486 100644 --- a/lib/src/core/multiquery_client.dart +++ b/lib/src/core/multiquery_client.dart @@ -1,4 +1,4 @@ -part of 'package:web3dart/web3dart.dart'; +part of '../../web3dart.dart'; class MultiQueryWeb3Client extends Web3Client { MultiQueryWeb3Client( diff --git a/lib/src/core/transaction.dart b/lib/src/core/transaction.dart index de7a59d7..e4b561af 100644 --- a/lib/src/core/transaction.dart +++ b/lib/src/core/transaction.dart @@ -1,4 +1,4 @@ -part of 'package:web3dart/web3dart.dart'; +part of '../../web3dart.dart'; class Transaction { Transaction({ diff --git a/lib/src/core/transaction_information.dart b/lib/src/core/transaction_information.dart index f5dcb20d..29af6fb8 100644 --- a/lib/src/core/transaction_information.dart +++ b/lib/src/core/transaction_information.dart @@ -1,4 +1,4 @@ -part of 'package:web3dart/web3dart.dart'; +part of '../../web3dart.dart'; class TransactionInformation { TransactionInformation.fromMap(Map map) diff --git a/lib/json_rpc.dart b/lib/src/rpc/json_rpc.dart similarity index 95% rename from lib/json_rpc.dart rename to lib/src/rpc/json_rpc.dart index 99df0ba8..6cbdb48f 100644 --- a/lib/json_rpc.dart +++ b/lib/src/rpc/json_rpc.dart @@ -1,11 +1,4 @@ -library json_rpc; - -import 'dart:async'; -import 'dart:convert'; - -import 'package:http/http.dart'; - -// ignore: one_member_abstracts +part of '../../web3dart.dart'; /// RPC Service base class. abstract class RpcService { diff --git a/lib/json_rpc_multiquery.dart b/lib/src/rpc/json_rpc_multiquery.dart similarity index 98% rename from lib/json_rpc_multiquery.dart rename to lib/src/rpc/json_rpc_multiquery.dart index 63540e31..a2d53c0a 100644 --- a/lib/json_rpc_multiquery.dart +++ b/lib/src/rpc/json_rpc_multiquery.dart @@ -1,4 +1,4 @@ -part of web3dart; +part of '../../web3dart.dart'; abstract class MultiQueryRpcService { /// Performs a single RPC request, asking the server to execute several queries diff --git a/lib/src/utils/length_tracking_byte_sink.dart b/lib/src/utils/length_tracking_byte_sink.dart index a3789b6b..661421aa 100644 --- a/lib/src/utils/length_tracking_byte_sink.dart +++ b/lib/src/utils/length_tracking_byte_sink.dart @@ -1,4 +1,4 @@ -part of 'package:web3dart/web3dart.dart'; +part of '../../web3dart.dart'; class LengthTrackingByteSink extends ByteConversionSinkBase { final Uint8Buffer _buffer = Uint8Buffer(); diff --git a/lib/web3dart.dart b/lib/web3dart.dart index 569c16b8..16d2e76c 100644 --- a/lib/web3dart.dart +++ b/lib/web3dart.dart @@ -21,7 +21,6 @@ import 'package:pointycastle/key_derivators/scrypt.dart' as scrypt; import 'package:pointycastle/src/utils.dart' as p_utils; import 'package:web3dart/web3dart.dart' as secp256k1; -import 'json_rpc.dart'; import 'src/core/block_number.dart'; import 'src/core/sync_information.dart'; @@ -36,13 +35,13 @@ export 'src/utils/typed_data.dart'; part 'src/core/eth_rpc_query/eth_rpc_query.dart'; part 'src/core/eth_rpc_query/params_classes.dart'; -part 'json_rpc_multiquery.dart'; part 'src/core/multiquery_client.dart'; part 'src/core/client.dart'; part 'src/core/filters.dart'; part 'src/core/transaction.dart'; part 'src/core/transaction_information.dart'; part 'src/core/transaction_signer.dart'; + part 'src/utils/length_tracking_byte_sink.dart'; part 'src/credentials/credentials.dart'; @@ -61,3 +60,6 @@ part 'src/crypto/formatting.dart'; part 'src/crypto/keccak.dart'; part 'src/crypto/random_bridge.dart'; part 'src/crypto/secp256k1.dart'; + +part 'src/rpc/json_rpc.dart'; +part 'src/rpc/json_rpc_multiquery.dart'; diff --git a/test/json_rpc_multiquery_test.dart b/test/json_rpc_multiquery_test.dart index f2adc945..f0302404 100644 --- a/test/json_rpc_multiquery_test.dart +++ b/test/json_rpc_multiquery_test.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:test/test.dart'; -import 'package:web3dart/json_rpc.dart'; import 'package:web3dart/web3dart.dart'; void main() { diff --git a/test/json_rpc_test.dart b/test/json_rpc_test.dart index aeb78461..e301e0bb 100644 --- a/test/json_rpc_test.dart +++ b/test/json_rpc_test.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:test/test.dart'; -import 'package:web3dart/json_rpc.dart'; +import 'package:web3dart/web3dart.dart'; final uri = Uri.parse('url');