diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f856d212..81cb5096 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: - name: Export FVM settings as environment variables uses: kuhnroyal/flutter-fvm-config-action@v1 - + - uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} diff --git a/docker-compose.yml b/docker-compose.yml index 53e529fa..75d7942d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,4 +4,4 @@ services: tests: build: context: . - dockerfile: docker/Dockerfile \ No newline at end of file + dockerfile: docker/Dockerfile diff --git a/melos.yaml b/melos.yaml index 0a711578..28b1e1f7 100644 --- a/melos.yaml +++ b/melos.yaml @@ -15,10 +15,10 @@ scripts: run: fvm dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'fvm dart test --coverage="coverage"' format: - run: fvm dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'fvm dart format --set-exit-if-changed .' + run: fvm dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'fvm dart format .' analyze: - run: fvm dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'fvm dart analyze .' + run: fvm dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'fvm dart analyze --fatal-infos .' # CI commands # OBS: CI we do not use FVM @@ -29,4 +29,4 @@ scripts: run: dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'dart format --set-exit-if-changed .' analyze:ci: - run: dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'dart analyze .' \ No newline at end of file + run: dart run melos exec --scope="${SCOPE:-*}" -c 1 --fail-fast -- 'dart analyze --fatal-infos .' \ No newline at end of file diff --git a/packages/polkadart/example/polkadart_example.dart b/packages/polkadart/example/polkadart_example.dart index 90ed9900..e9fe19f1 100644 --- a/packages/polkadart/example/polkadart_example.dart +++ b/packages/polkadart/example/polkadart_example.dart @@ -1,7 +1,7 @@ import 'package:polkadart/polkadart.dart' show Provider, StateApi; void main() async { - final polkadart = Provider(Uri.parse('wss://kusama-rpc.polkadot.io')); + final polkadart = Provider.fromUri(Uri.parse('wss://kusama-rpc.polkadot.io')); final api = StateApi(polkadart); final runtimeVersion = await api.getRuntimeVersion(); print(runtimeVersion.toJson()); diff --git a/packages/polkadart/lib/apis/apis.dart b/packages/polkadart/lib/apis/apis.dart index d76ffdf8..f3a2c80e 100644 --- a/packages/polkadart/lib/apis/apis.dart +++ b/packages/polkadart/lib/apis/apis.dart @@ -5,10 +5,14 @@ import 'dart:async' show Future, StreamSubscription; import 'package:convert/convert.dart' show hex; import '../primitives/primitives.dart' show - RuntimeVersion, - RuntimeMetadata, BlockHash, + ChainType, + Health, KeyValue, + RuntimeVersion, + RuntimeMetadata, + PeerInfo, + SyncState, StorageKey, StorageData, StorageChangeSet, @@ -16,3 +20,4 @@ import '../primitives/primitives.dart' import '../../provider.dart' show Provider; part './state.dart'; +part './system.dart'; diff --git a/packages/polkadart/lib/apis/state.dart b/packages/polkadart/lib/apis/state.dart index 3bbd86d3..3518fa3e 100644 --- a/packages/polkadart/lib/apis/state.dart +++ b/packages/polkadart/lib/apis/state.dart @@ -13,8 +13,8 @@ class StateApi

{ if (at != null) { params.add('0x${hex.encode(at)}'); } - final result = await _provider.send('state_call', params); - final data = result.result as String; + final response = await _provider.send('state_call', params); + final data = response.result as String; return Uint8List.fromList(hex.decode(data.substring(2))); } @@ -63,8 +63,8 @@ class StateApi

{ if (at != null) { params.add('0x${hex.encode(at)}'); } - final result = await _provider.send('state_getStorage', params); - final data = result.result as String?; + final response = await _provider.send('state_getStorage', params); + final data = response.result as String?; return data == null ? null : Uint8List.fromList(hex.decode(data.substring(2))); @@ -76,8 +76,8 @@ class StateApi

{ if (at != null) { params.add('0x${hex.encode(at)}'); } - final result = await _provider.send('state_getStorageHash', params); - final data = result.result as String?; + final response = await _provider.send('state_getStorageHash', params); + final data = response.result as String?; return data == null ? null : Uint8List.fromList(hex.decode(data.substring(2))); diff --git a/packages/polkadart/lib/apis/system.dart b/packages/polkadart/lib/apis/system.dart new file mode 100644 index 00000000..5cfec274 --- /dev/null +++ b/packages/polkadart/lib/apis/system.dart @@ -0,0 +1,153 @@ +part of apis; + +/// Substrate system RPC API +class SystemApi

{ + final P _provider; + + const SystemApi(this._provider); + + /// Get the node's implementation name. Plain old string. + Future name() async { + final response = await _provider.send('system_name', []); + return response.result as String; + } + + /// Get the node implementation's version. Should be a semver string. + Future version() async { + final response = await _provider.send('system_version', []); + return response.result as String; + } + + /// Get the chain's name. Given as a string identifier. + Future chain() async { + final response = await _provider.send('system_chain', []); + return response.result as String; + } + + /// Get the chain's type. + Future chainType() async { + final response = await _provider.send('system_chainType', []); + return ChainType.fromJson(response.result); + } + + /// Return health status of the node. + /// + /// Node is considered healthy if it is: + /// - connected to some peers (unless running in dev mode) + /// - not performing a major sync + Future health() async { + final response = await _provider.send('system_health', []); + return Health.fromJson(response.result as Map); + } + + /// Returns the base58-encoded PeerId of the node. + Future localPeerId() async { + final response = await _provider.send('system_localPeerId', []); + return response.result as String; + } + + /// Returns the multi-addresses that the local node is listening on + /// + /// The addresses include a trailing `/p2p/` with the local PeerId, and are thus suitable to + /// be passed to `addReservedPeer` or as a bootnode address for example. + Future> localListenAddresses() async { + final response = await _provider.send('system_localListenAddresses', []); + return (response.result as List).cast().toList(); + } + + /// Returns currently connected peers + /// + /// unsafe: This method is only active with appropriate flags + Future>> peers() async { + final response = await _provider.send('system_peers', []); + return (response.result as List) + .cast>() + .map((json) => PeerInfo.fromJson(json)) + .toList(); + } + + /// Returns the next valid index (aka nonce) for given account. + /// + /// This method takes into consideration all pending transactions + /// currently in the pool and if no transactions are found in the pool + /// it fallbacks to query the index from the runtime (aka. state nonce). + Future accountNextIndex(String account) async { + final response = await _provider.send('system_accountNextIndex', [account]); + return response.result as int; + } + + /// Dry run an extrinsic at a given block. Return SCALE encoded ApplyExtrinsicResult. + Future dryRun(Uint8List extrinsic, {BlockHash? at}) async { + final List params = ['0x${hex.encode(extrinsic)}']; + if (at != null) { + params.add('0x${hex.encode(at)}'); + } + final response = await _provider.send('system_dryRun', params); + final data = response.result as String; + return Uint8List.fromList(hex.decode(data.substring(2))); + } + + /// Returns the roles the node is running as. + Future> nodeRoles() async { + final response = await _provider.send('system_nodeRoles', []); + return (response.result as List).cast().toList(); + } + + /// Get a custom set of properties as a JSON object, defined in the chain spec. + Future> properties() async { + final response = await _provider.send('system_properties', []); + return response.result as Map; + } + + /// Adds a reserved peer. Returns the empty string or an error. The string + /// parameter should encode a `p2p` multiaddr. + /// + /// `/ip4/198.51.100.19/tcp/30333/p2p/QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV` + /// is an example of a valid, passing multiaddr with PeerId attached. + /// + /// unsafe: This method is only active with appropriate flags + Future addReservedPeer(String peer) async { + final response = await _provider.send('system_addReservedPeer', [peer]); + return response.result as String?; + } + + /// Remove a reserved peer. Returns the empty string or an error. The string + /// should encode only the PeerId e.g. `QmSk5HQbn6LhUwDiNMseVUjuRYhEtYj4aUZ6WfWoGURpdV`. + /// + /// unsafe: This method is only active with appropriate flags + Future removeReservedPeer(String peerId) async { + final response = + await _provider.send('system_removeReservedPeer', [peerId]); + return response.result as String?; + } + + /// Returns the list of reserved peers + Future> reservedPeers() async { + final response = await _provider.send('system_reservedPeers', []); + return (response.result as List).cast().toList(); + } + + /// Adds the supplied directives to the current log filter + /// + /// unsafe: This method is only active with appropriate flags + Future addLogFilter(String directives) async { + final response = await _provider.send('system_addLogFilter', [directives]); + return response.result; + } + + /// Resets the log filter to Substrate defaults + /// + /// unsafe: This method is only active with appropriate flags + Future resetLogFilter() async { + final response = await _provider.send('system_resetLogFilter', []); + return response.result; + } + + /// Returns the state of the syncing of the node: starting block, current best block, highest + /// known block. + Future syncState() async { + final response = await _provider.send('system_syncState', []); + final data = response.result as Map; + return SyncState.fromJson(data); + } +} diff --git a/packages/polkadart/lib/primitives/api_version.dart b/packages/polkadart/lib/primitives/api_version.dart index ef5028b8..c4d67f83 100644 --- a/packages/polkadart/lib/primitives/api_version.dart +++ b/packages/polkadart/lib/primitives/api_version.dart @@ -52,6 +52,16 @@ class ApiVersion { '0x${id.toRadixString(16)}', version, ]; + + @override + bool operator ==(Object other) => + other is ApiVersion && + other.runtimeType == runtimeType && + other.id == id && + other.version == version; + + @override + int get hashCode => Object.hash(id, version); } class $ApiVersionCodec with Codec { diff --git a/packages/polkadart/lib/primitives/chain_type.dart b/packages/polkadart/lib/primitives/chain_type.dart new file mode 100644 index 00000000..874ea41b --- /dev/null +++ b/packages/polkadart/lib/primitives/chain_type.dart @@ -0,0 +1,216 @@ +part of primitives; + +/// The type of a chain. +/// +/// This can be used by tools to determine the type of a chain for displaying +/// additional information or enabling additional features. +abstract class ChainType { + const ChainType(); + + static const $ChainTypeCodec codec = $ChainTypeCodec(); + + static const $ChainType values = $ChainType(); + + factory ChainType.decode(Input input) { + return codec.decode(input); + } + + factory ChainType.fromJson(dynamic json) { + String type; + if (json is String) { + type = json; + } else if (json is Map) { + type = json.keys.first; + if (type == 'Custom') { + return Custom(json[type]); + } + } else { + throw Exception('ChainType: Invalid json value: "$json"'); + } + switch (type) { + case 'Development': + return const Development(); + case 'Local': + return const Local(); + case 'Live': + return const Live(); + default: + throw Exception('ChainType: unknown type "$type"'); + } + } + + Uint8List encode() { + final output = ByteOutput(codec.sizeHint(this)); + codec.encodeTo(this, output); + return output.toBytes(); + } + + int sizeHint() { + return codec.sizeHint(this); + } + + Map toJson(); +} + +/// ChainType enum values +class $ChainType { + const $ChainType(); + + Development development() { + return const Development(); + } + + Local local() { + return const Local(); + } + + Live live() { + return const Live(); + } + + Custom custom(String value) { + return Custom(value); + } +} + +/// ChainType Scale Codec +class $ChainTypeCodec with Codec { + const $ChainTypeCodec(); + + @override + ChainType decode(Input input) { + final index = U8Codec.codec.decode(input); + switch (index) { + case 0: + return const Development(); + case 1: + return const Local(); + case 2: + return const Live(); + case 3: + return Custom._decode(input); + default: + throw Exception('ChainType: Invalid variant index: "$index"'); + } + } + + @override + void encodeTo( + ChainType value, + Output output, + ) { + switch (value.runtimeType) { + case Development: + (value as Development).encodeTo(output); + break; + case Local: + (value as Local).encodeTo(output); + break; + case Live: + (value as Live).encodeTo(output); + break; + case Custom: + (value as Custom).encodeTo(output); + break; + default: + throw Exception( + 'ChainType: Unsupported "$value" of type "${value.runtimeType}"'); + } + } + + @override + int sizeHint(ChainType value) { + switch (value.runtimeType) { + case Development: + case Local: + case Live: + return 1; + case Custom: + return (value as Custom)._sizeHint(); + default: + throw Exception( + 'ChainType: Unsupported "$value" of type "${value.runtimeType}"'); + } + } +} + +/// A development chain that runs mainly on one node. +class Development extends ChainType { + const Development(); + + @override + Map toJson() => {'Development': null}; + void encodeTo(Output output) { + U8Codec.codec.encodeTo( + 0, + output, + ); + } +} + +/// A local chain that runs locally on multiple nodes for testing purposes. +class Local extends ChainType { + const Local(); + + @override + Map toJson() => {'Local': null}; + + void encodeTo(Output output) { + U8Codec.codec.encodeTo( + 1, + output, + ); + } +} + +/// A live chain. +class Live extends ChainType { + const Live(); + + @override + Map toJson() => {'Live': null}; + void encodeTo(Output output) { + U8Codec.codec.encodeTo( + 2, + output, + ); + } +} + +/// Some custom chain type. +class Custom extends ChainType { + const Custom(this.value); + + factory Custom._decode(Input input) { + return Custom(StrCodec.codec.decode(input)); + } + + final String value; + + @override + Map toJson() => {'Custom': value}; + + int _sizeHint() { + return 1 + StrCodec.codec.sizeHint(value); + } + + void encodeTo(Output output) { + U8Codec.codec.encodeTo( + 3, + output, + ); + StrCodec.codec.encodeTo( + value, + output, + ); + } + + @override + bool operator ==(Object other) => + other is Custom && + other.runtimeType == runtimeType && + other.value == value; + + @override + int get hashCode => value.hashCode; +} diff --git a/packages/polkadart/lib/primitives/health.dart b/packages/polkadart/lib/primitives/health.dart new file mode 100644 index 00000000..a240ed6a --- /dev/null +++ b/packages/polkadart/lib/primitives/health.dart @@ -0,0 +1,91 @@ +part of primitives; + +/// Health struct returned by the RPC +class Health { + const Health({ + required this.peers, + required this.isSyncing, + required this.shouldHavePeers, + }); + + factory Health.decode(Input input) { + return codec.decode(input); + } + + factory Health.fromJson(Map json) { + return Health( + peers: json['peers'] as int, + isSyncing: json['isSyncing'] as bool, + shouldHavePeers: json['shouldHavePeers'] as bool, + ); + } + + /// Number of connected peers + final int peers; + + /// Is the node syncing + final bool isSyncing; + + /// Should this node have any peers + /// + /// Might be false for local chains or when running without discovery. + final bool shouldHavePeers; + + static const $HealthCodec codec = $HealthCodec(); + + Uint8List encode() { + return codec.encode(this); + } + + @override + bool operator ==(Object other) => + other is Health && + other.runtimeType == runtimeType && + other.peers == peers && + other.isSyncing == isSyncing && + other.shouldHavePeers == shouldHavePeers; + + @override + int get hashCode => Object.hash(peers, isSyncing, shouldHavePeers); +} + +class $HealthCodec with Codec { + const $HealthCodec(); + + @override + void encodeTo( + Health value, + Output output, + ) { + U32Codec.codec.encodeTo( + value.peers, + output, + ); + BoolCodec.codec.encodeTo( + value.isSyncing, + output, + ); + BoolCodec.codec.encodeTo( + value.shouldHavePeers, + output, + ); + } + + @override + Health decode(Input input) { + return Health( + peers: U32Codec.codec.decode(input), + isSyncing: BoolCodec.codec.decode(input), + shouldHavePeers: BoolCodec.codec.decode(input), + ); + } + + @override + int sizeHint(Health value) { + int size = 0; + size += U32Codec.codec.sizeHint(value.peers); + size += BoolCodec.codec.sizeHint(value.isSyncing); + size += BoolCodec.codec.sizeHint(value.shouldHavePeers); + return size; + } +} diff --git a/packages/polkadart/lib/primitives/peer_info.dart b/packages/polkadart/lib/primitives/peer_info.dart new file mode 100644 index 00000000..b7f51a10 --- /dev/null +++ b/packages/polkadart/lib/primitives/peer_info.dart @@ -0,0 +1,101 @@ +part of primitives; + +/// Network Peer information +class PeerInfo { + const PeerInfo({ + required this.peerId, + required this.roles, + required this.bestHash, + required this.bestNumber, + }); + + factory PeerInfo.fromJson(Map json) { + return PeerInfo( + peerId: json['peerId'] as String, + roles: json['roles'] as String, + bestHash: json['bestHash'] as H, + bestNumber: json['bestNumber'] as N, + ); + } + + /// Peer ID + final String peerId; + + /// Roles + final String roles; + + /// Peer best block hash + final H bestHash; + + /// Peer best block number + final N bestNumber; + + Map toJson() => { + 'peerId': peerId, + 'roles': roles, + 'bestHash': bestHash, + 'bestNumber': bestNumber, + }; + + @override + bool operator ==(Object other) => + other is PeerInfo && + other.peerId == peerId && + other.roles == roles && + other.bestHash == bestHash && + other.bestNumber == bestNumber; + + @override + int get hashCode => Object.hash(peerId, roles, bestHash, bestNumber); +} + +/// PeerInfo Scale Codec +class PeerInfoCodec with Codec> { + final Codec hashCodec; + final Codec numberCodec; + + const PeerInfoCodec({required this.hashCodec, required this.numberCodec}); + + @override + void encodeTo( + PeerInfo value, + Output output, + ) { + StrCodec.codec.encodeTo( + value.peerId, + output, + ); + StrCodec.codec.encodeTo( + value.roles, + output, + ); + hashCodec.encodeTo( + value.bestHash, + output, + ); + numberCodec.encodeTo( + value.bestNumber, + output, + ); + } + + @override + PeerInfo decode(Input input) { + return PeerInfo( + peerId: StrCodec.codec.decode(input), + roles: StrCodec.codec.decode(input), + bestHash: hashCodec.decode(input), + bestNumber: numberCodec.decode(input), + ); + } + + @override + int sizeHint(PeerInfo value) { + int size = 0; + size += StrCodec.codec.sizeHint(value.peerId); + size += StrCodec.codec.sizeHint(value.roles); + size += hashCodec.sizeHint(value.bestHash); + size += numberCodec.sizeHint(value.bestNumber); + return size; + } +} diff --git a/packages/polkadart/lib/primitives/primitives.dart b/packages/polkadart/lib/primitives/primitives.dart index 92f06ead..d6f11db7 100644 --- a/packages/polkadart/lib/primitives/primitives.dart +++ b/packages/polkadart/lib/primitives/primitives.dart @@ -5,12 +5,14 @@ import 'package:convert/convert.dart' show hex; import 'package:substrate_metadata/utils/utils.dart' show ToJson; import 'package:polkadart_scale_codec/polkadart_scale_codec.dart' show + ByteInput, + ByteOutput, + BoolCodec, Codec, Input, - ByteInput, Output, - U64Codec, U32Codec, + U64Codec, U8Codec, SequenceCodec, StrCodec; @@ -19,6 +21,10 @@ import 'package:substrate_metadata/substrate_metadata.dart' import '../substrate/substrate.dart' show Hasher; part './api_version.dart'; +part './chain_type.dart'; +part './health.dart'; +part './peer_info.dart'; part './runtime_metadata.dart'; part './runtime_version.dart'; part './storage.dart'; +part './sync_state.dart'; diff --git a/packages/polkadart/lib/primitives/runtime_version.dart b/packages/polkadart/lib/primitives/runtime_version.dart index 7c388892..3110ff73 100644 --- a/packages/polkadart/lib/primitives/runtime_version.dart +++ b/packages/polkadart/lib/primitives/runtime_version.dart @@ -105,6 +105,24 @@ class RuntimeVersion { 'transactionVersion': transactionVersion, 'stateVersion': stateVersion, }; + + @override + bool operator ==(Object other) => + other is RuntimeVersion && + other.runtimeType == runtimeType && + other.specName == specName && + other.implName == implName && + other.authoringVersion == authoringVersion && + other.specVersion == specVersion && + other.implName == implName && + other.apis.length == apis.length && + other.apis == apis && + other.transactionVersion == transactionVersion && + other.stateVersion == stateVersion; + + @override + int get hashCode => Object.hash(specName, implName, authoringVersion, + specVersion, implName, apis, transactionVersion, stateVersion); } class $RuntimeVersionCodec with Codec { diff --git a/packages/polkadart/lib/primitives/sync_state.dart b/packages/polkadart/lib/primitives/sync_state.dart new file mode 100644 index 00000000..31d40841 --- /dev/null +++ b/packages/polkadart/lib/primitives/sync_state.dart @@ -0,0 +1,87 @@ +part of primitives; + +/// The state of the syncing of the node. +class SyncState { + const SyncState({ + required this.startingBlock, + required this.currentBlock, + required this.highestBlock, + }); + + factory SyncState.fromJson(Map json) { + return SyncState( + startingBlock: json['startingBlock'] as int, + currentBlock: json['currentBlock'] as int, + highestBlock: json['highestBlock'] as int, + ); + } + + /// Height of the block at which syncing started. + final int startingBlock; + + /// Height of the current best block of the node. + final int currentBlock; + + /// Height of the highest block in the network. + final int highestBlock; + + Map toJson() => { + 'startingBlock': startingBlock, + 'currentBlock': currentBlock, + 'highestBlock': highestBlock, + }; + + @override + bool operator ==(Object other) => + other is SyncState && + other.runtimeType == runtimeType && + other.startingBlock == startingBlock && + other.currentBlock == currentBlock && + other.highestBlock == highestBlock; + + @override + int get hashCode => Object.hash(startingBlock, currentBlock, highestBlock); +} + +/// SyncState Scale Codec +class SyncStateCodec with Codec { + final Codec numberCodec; + + const SyncStateCodec({required this.numberCodec}); + + @override + void encodeTo( + SyncState value, + Output output, + ) { + numberCodec.encodeTo( + value.startingBlock, + output, + ); + numberCodec.encodeTo( + value.currentBlock, + output, + ); + numberCodec.encodeTo( + value.highestBlock, + output, + ); + } + + @override + SyncState decode(Input input) { + return SyncState( + startingBlock: numberCodec.decode(input), + currentBlock: numberCodec.decode(input), + highestBlock: numberCodec.decode(input), + ); + } + + @override + int sizeHint(SyncState value) { + int size = numberCodec.sizeHint(value.startingBlock); + size += numberCodec.sizeHint(value.currentBlock); + size = numberCodec.sizeHint(value.highestBlock); + return size; + } +} diff --git a/packages/polkadart/lib/provider.dart b/packages/polkadart/lib/provider.dart index 1dcd9da1..9d443a17 100644 --- a/packages/polkadart/lib/provider.dart +++ b/packages/polkadart/lib/provider.dart @@ -30,9 +30,9 @@ class SubscriptionMessage { // Generic transport providers to handle the transport of method calls to and from Polkadot clients from applications interacting with it. abstract class Provider { - const Provider._(); + const Provider(); - factory Provider(Uri uri) { + factory Provider.fromUri(Uri uri) { if (uri.scheme == 'http' || uri.scheme == 'https') { return HttpProvider(uri); } @@ -63,7 +63,7 @@ abstract class Provider { /// It does not support subscriptions so you won't be able to listen to events such as new blocks or balance changes. /// It is usually preferable using the [[WsProvider]] class HttpProvider extends Provider { - HttpProvider(this.url) : super._(); + HttpProvider(this.url) : super(); // uri to connect to final Uri url; @@ -113,7 +113,7 @@ class HttpProvider extends Provider { /// it does support subscriptions and allows listening to events such as new blocks or balance changes. class WsProvider extends Provider { /// Creates a new websocket connection, connects automatically by default - WsProvider(this.url, {bool autoConnect = true}) : super._() { + WsProvider(this.url, {bool autoConnect = true}) : super() { if (autoConnect) { connect(); } @@ -122,7 +122,7 @@ class WsProvider extends Provider { /// The endpoint url final Uri url; - /// Maps de query id to the completer that will resolve the query + /// Maps the query id to the completer that will resolve the query final Map> queries = {}; /// Maps the subscription id to the stream controller that will emit the subscription data diff --git a/packages/polkadart/test/apis/mock_provider.dart b/packages/polkadart/test/apis/mock_provider.dart new file mode 100644 index 00000000..2467f929 --- /dev/null +++ b/packages/polkadart/test/apis/mock_provider.dart @@ -0,0 +1,57 @@ +import 'dart:async' show Future, FutureOr; +import 'package:polkadart/polkadart.dart' + show Provider, RpcResponse, SubscriptionReponse; + +/// The Mock Provider allows mock requests. +class MockProvider extends Provider { + MockProvider(this._state) : super(); + + /// Custom State + final S _state; + + /// Maps the methods to the mock responses + final Map, S)> _callbacks = {}; + + // Sequence used to generate unique query ids + int _sequence = 0; + + void setMethodCallback( + String method, dynamic Function(List, S) callback) { + _callbacks[method] = callback; + } + + @override + Future send(String method, List params) async { + if (_callbacks[method] == null) { + throw Exception( + 'MockProvider: The callback for the method "$method" isn\'t defined'); + } + + final response = _callbacks[method]!(params, _state); + return RpcResponse( + id: ++_sequence, + result: response, + ); + } + + @override + Future subscribe(String method, List params, + {FutureOr Function(String subscription)? onCancel}) { + throw Exception('MockProvider does not support subscriptions'); + } + + @override + Future connect() { + return Future.value(); + } + + @override + Future disconnect() { + return Future.value(); + } + + @override + bool isConnected() { + return true; + } +} diff --git a/packages/polkadart/test/apis/system_test.dart b/packages/polkadart/test/apis/system_test.dart new file mode 100644 index 00000000..3ad88bec --- /dev/null +++ b/packages/polkadart/test/apis/system_test.dart @@ -0,0 +1,160 @@ +import 'package:polkadart/polkadart.dart' + show SystemApi, ChainType, Health, PeerInfo, SyncState; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; +import './mock_provider.dart' show MockProvider; + +void main() { + group('SystemApi', () { + test('name', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_name', (params, state) => 'Parity Polkadot'); + expect(api.name(), completion('Parity Polkadot')); + }); + + test('version', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_version', (params, state) => '0.9.43-ba42b9ce51d'); + expect(api.version(), completion('0.9.43-ba42b9ce51d')); + }); + + test('chain', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback('system_chain', (params, state) => 'Polkadot'); + expect(api.chain(), completion('Polkadot')); + }); + + test('chainType', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_chainType', (params, state) => 'Development'); + expect(api.chainType(), completion(ChainType.values.development())); + + provider.setMethodCallback('system_chainType', (params, state) => 'Live'); + expect(api.chainType(), completion(ChainType.values.live())); + + provider.setMethodCallback( + 'system_chainType', (params, state) => 'Local'); + expect(api.chainType(), completion(ChainType.values.local())); + + provider.setMethodCallback( + 'system_chainType', (params, state) => {'Custom': 'polkadart 1337'}); + expect(api.chainType(), + completion(ChainType.values.custom('polkadart 1337'))); + }); + + test('health', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_health', + (params, state) => + {'peers': 22, 'isSyncing': false, 'shouldHavePeers': true}); + expect( + api.health(), + completion( + Health(peers: 22, isSyncing: false, shouldHavePeers: true))); + }); + + test('localPeerId', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_localPeerId', + (params, state) => + '12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby'); + expect(api.localPeerId(), + completion('12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby')); + }); + + test('localListenAddresses', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_localListenAddresses', + (params, state) => [ + '/ip6/::1/tcp/34102/ws/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip6/2800:c20:0:11::66/tcp/34102/ws/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip6/fe80::4c41:2cff:fe96:5f75/tcp/34102/ws/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip4/127.0.0.1/tcp/33102/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip4/192.168.0.102/tcp/33102/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip6/::1/tcp/33102/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + ]); + expect( + api.localListenAddresses(), + completion([ + '/ip6/::1/tcp/34102/ws/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip6/2800:c20:0:11::66/tcp/34102/ws/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip6/fe80::4c41:2cff:fe96:5f75/tcp/34102/ws/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip4/127.0.0.1/tcp/33102/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip4/192.168.0.102/tcp/33102/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + '/ip6/::1/tcp/33102/p2p/12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + ])); + }); + + test('peers', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_peers', + (params, state) => [ + { + 'peerId': + '12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + 'roles': 'Full', + 'bestHash': + '0x1a162f9495422abd65e48fc4768bccfd3d19c9ad71009975738bd4c0dd5bfdb3', + 'bestNumber': 1000 + } + ]); + expect( + api.peers(), + completion([ + PeerInfo( + peerId: '12D3KooWBDg7u6dBEo82fJe4kyDJT3L2C8kYNYGNvuBkEPVryKby', + roles: 'Full', + bestHash: + '0x1a162f9495422abd65e48fc4768bccfd3d19c9ad71009975738bd4c0dd5bfdb3', + bestNumber: 1000) + ])); + }); + + test('accountNextIndex', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback('system_accountNextIndex', (params, state) { + assert(params.length == 1); + assert(params[0] == '15kUt2i86LHRWCkE3D9Bg1HZAoc2smhn1fwPzDERTb1BXAkX'); + return 258938; + }); + expect( + api.accountNextIndex( + '15kUt2i86LHRWCkE3D9Bg1HZAoc2smhn1fwPzDERTb1BXAkX'), + completion(258938)); + }); + + test('syncState', () { + final provider = MockProvider(null); + final api = SystemApi(provider); + provider.setMethodCallback( + 'system_syncState', + (params, state) => { + 'startingBlock': 16299243, + 'currentBlock': 16313588, + 'highestBlock': 16313589 + }); + expect( + api.syncState(), + completion(SyncState( + startingBlock: 16299243, + currentBlock: 16313588, + highestBlock: 16313589))); + }); + }); +} diff --git a/packages/polkadart_cli/bin/generate.dart b/packages/polkadart_cli/bin/generate.dart index d6e53906..72d876ec 100644 --- a/packages/polkadart_cli/bin/generate.dart +++ b/packages/polkadart_cli/bin/generate.dart @@ -16,7 +16,7 @@ class ChainProperties { ChainProperties(this.metadata, this.version); static Future fromURL(Uri uri) async { - final provider = Provider(uri); + final provider = Provider.fromUri(uri); final api = StateApi(provider); final decodedMetadata = await api.getMetadata(); if (decodedMetadata.version != 14) { @@ -33,7 +33,7 @@ class ChainProperties { } Future chainProperties(Uri url) async { - final provider = Provider(url); + final provider = Provider.fromUri(url); final api = StateApi(provider); final decodedMetadata = await api.getMetadata(); if (decodedMetadata.version != 14) { diff --git a/packages/polkadart_cli/lib/src/generator/polkadart.dart b/packages/polkadart_cli/lib/src/generator/polkadart.dart index 0dccb43f..978158d6 100644 --- a/packages/polkadart_cli/lib/src/generator/polkadart.dart +++ b/packages/polkadart_cli/lib/src/generator/polkadart.dart @@ -78,16 +78,27 @@ class PolkadartGenerator { ..name = 'Rpc' ..constructors.add(Constructor((b) => b ..constant = true - ..optionalParameters.add(Parameter((b) => b - ..toThis = true - ..required = true - ..named = true - ..name = 'state')))) + ..optionalParameters.addAll([ + Parameter((b) => b + ..toThis = true + ..required = true + ..named = true + ..name = 'state'), + Parameter((b) => b + ..toThis = true + ..required = true + ..named = true + ..name = 'system') + ]))) ..fields.addAll([ Field((b) => b ..name = 'state' ..type = refs.stateApi - ..modifier = FieldModifier.final$) + ..modifier = FieldModifier.final$), + Field((b) => b + ..name = 'system' + ..type = refs.systemApi + ..modifier = FieldModifier.final$), ]); }); @@ -127,7 +138,8 @@ class PolkadartGenerator { ..body = Block.of([ declareFinal('rpc') .assign(refer('Rpc').newInstance([], { - 'state': refs.stateApi.newInstance([refer('provider')]) + 'state': refs.stateApi.newInstance([refer('provider')]), + 'system': refs.systemApi.newInstance([refer('provider')]), })) .statement, refer(name) diff --git a/packages/polkadart_cli/lib/src/typegen/references.dart b/packages/polkadart_cli/lib/src/typegen/references.dart index 8779eeda..186b4151 100644 --- a/packages/polkadart_cli/lib/src/typegen/references.dart +++ b/packages/polkadart_cli/lib/src/typegen/references.dart @@ -246,6 +246,7 @@ const storageHasher = Reference('StorageHasher', 'package:polkadart/polkadart.dart'); const provider = Reference('Provider', 'package:polkadart/polkadart.dart'); const stateApi = Reference('StateApi', 'package:polkadart/polkadart.dart'); +const systemApi = Reference('SystemApi', 'package:polkadart/polkadart.dart'); const blockHash = Reference('BlockHash', 'package:polkadart/polkadart.dart'); TypeReference storageValue(Reference value) {