diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index 7c1e071e..f877f1bf 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -205,6 +205,12 @@ "packageUri": "lib/", "languageVersion": "2.18" }, + { + "name": "hashlib", + "rootUri": "file:///home/alaa/.pub-cache/hosted/pub.dev/hashlib-1.16.0", + "packageUri": "lib/", + "languageVersion": "2.14" + }, { "name": "hashlib_codecs", "rootUri": "file:///home/alaa/.pub-cache/hosted/pub.dev/hashlib_codecs-2.2.0", @@ -391,6 +397,12 @@ "packageUri": "lib/", "languageVersion": "2.12" }, + { + "name": "protobuf", + "rootUri": "file:///home/alaa/.pub-cache/hosted/pub.dev/protobuf-3.1.0", + "packageUri": "lib/", + "languageVersion": "2.19" + }, { "name": "pub_semver", "rootUri": "file:///home/alaa/.pub-cache/hosted/pub.dev/pub_semver-2.1.4", @@ -433,6 +445,12 @@ "packageUri": "lib/", "languageVersion": "3.0" }, + { + "name": "rmb_client", + "rootUri": "../packages/rmb_client", + "packageUri": "lib/", + "languageVersion": "3.2" + }, { "name": "secp256k1_ecdsa", "rootUri": "file:///home/alaa/.pub-cache/hosted/pub.dev/secp256k1_ecdsa-0.4.0", @@ -656,7 +674,8 @@ "languageVersion": "3.2" } ], - "generated": "2024-04-18T09:13:15.973441Z", + "generated": "2024-06-02T07:16:24.662504Z", "generator": "pub", - "generatorVersion": "3.3.3" + "generatorVersion": "3.4.0", + "pubCache": "file:///home/alaa/.pub-cache" } diff --git a/melos.yaml b/melos.yaml index dec8d848..b7581420 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,5 +1,6 @@ name: tfgrid_sdk_dart_monorepo packages: + - ./packages/rmb_client - packages/** - ./packages/signer - ./packages/tfchain_client diff --git a/packages/rmb_client/.gitignore b/packages/rmb_client/.gitignore new file mode 100644 index 00000000..3a857904 --- /dev/null +++ b/packages/rmb_client/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/packages/rmb_client/CHANGELOG.md b/packages/rmb_client/CHANGELOG.md new file mode 100644 index 00000000..effe43c8 --- /dev/null +++ b/packages/rmb_client/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/rmb_client/README.md b/packages/rmb_client/README.md new file mode 100644 index 00000000..3816eca3 --- /dev/null +++ b/packages/rmb_client/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/packages/rmb_client/analysis_options.yaml b/packages/rmb_client/analysis_options.yaml new file mode 100644 index 00000000..dee8927a --- /dev/null +++ b/packages/rmb_client/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/rmb_client/bin/rmb_client.dart b/packages/rmb_client/bin/rmb_client.dart new file mode 100644 index 00000000..8bb9b7bb --- /dev/null +++ b/packages/rmb_client/bin/rmb_client.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:rmb_client/rmb_client.dart'; + +void main() async { + final client = Client( + relayUrl: "wss://relay.dev.grid.tf/", + chainUrl: "wss://tfchain.dev.grid.tf/ws", + mnemonic: + "valid end trumpet hunt produce close hire virus fee rebel gentle claim", + session: "testclient", + retries: 3, + keypairType: "sr25519"); + + await client.connect(); + // final ID = await client.send("zos.statistics.get", "{}", 17, 5, 3); + + final ID = + await client.send("twinserver.balance.getMyBalance", "{}", 7845, 5, 3); + + // sleep(Duration(seconds: 20)); + + // await client.read(ID!); + + // await client.send("requestCommand", "requestData", 17, 5, 0); + + // client.closeConnection(); +} diff --git a/packages/rmb_client/lib/rmb_client.dart b/packages/rmb_client/lib/rmb_client.dart new file mode 100644 index 00000000..f1b21f79 --- /dev/null +++ b/packages/rmb_client/lib/rmb_client.dart @@ -0,0 +1,24 @@ +library rmb_client; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:fixnum/fixnum.dart'; +import 'package:polkadart_keyring/polkadart_keyring.dart'; +import 'package:rmb_client/src/envelope.dart'; +import 'package:rmb_client/src/sign.dart'; +import 'package:rmb_client/src/utils.dart'; +import 'package:rmb_client/types/generated/types.pbserver.dart'; +import 'package:tfchain_client/generated/dev/types/pallet_collective/raw_origin.dart'; +import 'package:tfchain_client/generated/dev/types/pallet_tfgrid/types/twin.dart'; +import 'package:tfchain_client/generated/dev/types/tfchain_runtime/runtime_event.dart'; +import 'package:tfchain_client/models/twins.dart'; +import 'package:uuid/uuid.dart'; +import 'package:web_socket_channel/status.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:tfchain_client/tfchain_client.dart' as TFClient; +import 'package:async_locks/async_locks.dart'; +import 'dart:async'; + +part 'src/client.dart'; diff --git a/packages/rmb_client/lib/src/client.dart b/packages/rmb_client/lib/src/client.dart new file mode 100644 index 00000000..9c3e8886 --- /dev/null +++ b/packages/rmb_client/lib/src/client.dart @@ -0,0 +1,379 @@ +part of "../rmb_client.dart"; + +class TwinsMap { + Twin twin; + int timestamp; + + TwinsMap(this.twin, this.timestamp); + @override + String toString() { + return 'TwinsMap(twin: ${twin.id}, timestamp: $timestamp)'; + } +} + +class Client { + static final Map _connections = {}; + static final connectingLock = Lock(); + KeyPair? signer; + Address source = Address(); + static final Map responses = {}; + WebSocket? socket; + Twin? twin; + Twin? destTwin; + TFClient.QueryClient? tfClient; + Map? twins; + String relayUrl; + String chainUrl; + String mnemonic; + String session; + int? retries; + String keypairType; + Timer? pingTimer; + + Client._internal( + this.relayUrl, + this.chainUrl, + this.mnemonic, + this.session, { + this.retries = 5, + required this.keypairType, + this.twins, + }) { + tfClient = TFClient.QueryClient(chainUrl); + } + + factory Client({ + required String relayUrl, + required String chainUrl, + required String mnemonic, + required String session, + int retries = 5, + required String keypairType, + Map? twins, + }) { + final key = '$relayUrl:$mnemonic:$keypairType'; + if (_connections.containsKey(key)) { + return _connections[key]!; + } + + if (keypairType != KPType.ed25519 && keypairType != KPType.sr25519) { + throw UnsupportedError('Unsupported Keypair type'); + } + + final newClient = Client._internal( + relayUrl, + chainUrl, + mnemonic, + session, + retries: retries, + keypairType: keypairType, + twins: twins, + ); + + _connections[key] = newClient; + return newClient; + } + + Future createConnection() async { + if (socket != null && socket!.closeCode == WebSocket.closed) { + await socket!.close(); + } + + try { + final wsUrl = await updateUrl(); + + if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) { + throw Exception('Invalid WebSocket URL: $wsUrl'); + } + + try { + socket = await WebSocket.connect(wsUrl); + if (socket!.readyState == WebSocket.open) { + socket!.listen((message) async { + print("HELLO FROM Listening to response"); + + final recievedEnvelope = Envelope.fromBuffer(message); + print("Envelope Received: ${recievedEnvelope.hasResponse()}"); + + final castedEnvelope = ClientEnvelope( + envelope: recievedEnvelope, + chainUrl: chainUrl, + tfClient: tfClient!, + twins: twins!); + + if (responses.containsKey(recievedEnvelope.uid)) { + responses[recievedEnvelope.uid] = castedEnvelope; + print("update envelope in responses map"); + // TODO: + await read(recievedEnvelope.uid); + } + }); + } else { + print( + 'Error: Could not open connection (readyState: ${socket!.readyState})'); + } + } catch (error) { + throw Exception("Unable to start connection $error"); + } + } catch (error) { + throw Exception('Unable to create websocket connection due to $error'); + } + } + + Future waitForOpenConnection() { + final completer = Completer(); + const maxNumberOfAttempts = 20; + const intervalTime = Duration(milliseconds: 500); + + var currentAttempt = 0; + Timer? timer; + + timer = Timer.periodic(intervalTime, (timer) { + if (currentAttempt > maxNumberOfAttempts - 1) { + timer.cancel(); + completer.completeError( + throw Exception("Maximum number of attempts exceeded")); + } else if (socket != null) { + timer.cancel(); + completer.complete(); + } + currentAttempt++; + }); + + return completer.future; + } + + Future connect() async { + try { + await Client.connectingLock.acquire(); + if (socket != null && socket!.closeCode == WebSocket.closed) { + print("Already Connected!"); + return; + } + + tfClient?.connect(); + await createSigner(); + + final twinId = await tfClient?.twins.getTwinIdByAccountId( + QueryTwinsGetTwinByAccountIdOptions( + accountId: signer!.publicKey.bytes)); + if (twinId == 0) { + throw "Couldn't find a user for the provided mnemonic on this network."; + } + twin = await tfClient?.twins.get(QueryTwinsGetOptions(id: twinId!)); + twins ??= {}; + twins![twin!.id.hashCode] = TwinsMap( + twin!, (DateTime.now().millisecondsSinceEpoch / 1000).round()); + try { + updateSource(); + await createConnection(); + } catch (error) { + throw Exception( + "Unable to establish connection with rmb relay: $error"); + } + } catch (e) { + throw Exception("Error Connecting on relay : $e"); + } + // finally { + // connectingLock.release(); + // } + } + + Future newJWT(String session) async { + Map header = { + 'alg': 'RS512', + 'typ': 'JWT', + }; + + int now = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000; + // TODO: TwinId should bs passed from params + Map claims = { + 'sub': 7858, + 'iat': now, + 'exp': now + 1000, + 'sid': session, + }; + + String jwt = + '${base64Url.encode(utf8.encode(jsonEncode(header)))}.${base64Url.encode(utf8.encode(jsonEncode(claims)))}'; + + Uint8List sigPrefixed = sign(Uint8List.fromList(jwt.codeUnits), signer!); + String token = '$jwt.${base64Url.encode(sigPrefixed)}'; + + return token; + } + + void updateSource() { + source.twin = twin!.id; + source.connection = session; + } + + Future ping() async { + try { + var uuid = Uuid(); + + final pingEnvelope = Envelope( + uid: uuid.v4(), + timestamp: + Int64((DateTime.now().microsecondsSinceEpoch / 1000).round()), + expiration: Int64(40), + source: source, + ping: Ping()); + + pingEnvelope.destination = Address(); + + final clientEnvelope = ClientEnvelope( + signer: signer, + envelope: pingEnvelope, + chainUrl: chainUrl, + tfClient: tfClient!, + twins: twins!); + + var retriesCount = 0; + while (socket!.readyState != WebSocket.open && retries! >= retriesCount) { + try { + await waitForOpenConnection(); + } catch (error) { + if (retries == retriesCount) { + throw Exception( + 'Failed to open connection after $retriesCount retries.'); + } + await createConnection(); + } + } + + responses[clientEnvelope.uid] = clientEnvelope; + socket!.add(clientEnvelope.envelope.writeToBuffer()); + return clientEnvelope.uid; + } catch (error) { + throw Exception('Unable to send due to $error'); + } + } + + Future send(String requestCommand, var requestData, + int destinationTwinId, int expirationMinutes, int? retries) async { + try { + var uuid = Uuid(); + destTwin = await getTwin(destinationTwinId, twins!, tfClient!); + + // create new envelope with given data and destination + final envelope = Envelope( + uid: uuid.v4(), + timestamp: + Int64((DateTime.now().microsecondsSinceEpoch / 1000).round()), + expiration: Int64(expirationMinutes * 60), + source: source); + envelope.destination = Address(twin: destTwin?.id); + if (requestCommand.isNotEmpty) { + envelope.request = Request(command: requestCommand); + } + final clientEnvelope = ClientEnvelope( + signer: signer, + envelope: envelope, + chainUrl: chainUrl, + tfClient: tfClient!, + twins: twins!); + + var retriesCount = 0; + if (requestData != null && requestData.toString().isNotEmpty) { + clientEnvelope.plain = Uint8List.fromList(utf8.encode(requestData)); + } else { + clientEnvelope.plain = Uint8List(0); + } + + // TODO: this cause stackoverflow + // clientEnvelope.relays = + // twin!.relay?.map((i) => i.toString()).toList() ?? []; + + // print("HEllo after ading relays"); + // print('Relays added to client envelope: ${clientEnvelope.relays}'); + + if (signer != null) { + print("Is It signed ???"); + clientEnvelope.signature = clientEnvelope.signEnvelope(); + } + responses[clientEnvelope.uid] = clientEnvelope; + + try { + if (socket!.readyState == WebSocket.open) { + socket!.add(clientEnvelope.envelope.writeToBuffer()); + print('Message sent successfully.'); + } else { + print("Error: WebSocket connection not open."); + } + } catch (error) { + throw Exception("Couldn't send envelope to RMB. $error"); + } + + return clientEnvelope.uid; + } catch (error) { + throw Exception("Unable to send msg to RMB dut to $error"); + } + } + + Future read(String requestID) async { + var envelope = responses[requestID]; + final now = DateTime.now().millisecondsSinceEpoch; + while (envelope != null && + DateTime.now().millisecondsSinceEpoch < + now + envelope.expiration!.toInt() * 1000) { + envelope = responses[requestID]; + + if (envelope != null && envelope.envelope.hasResponse()) { + print("Let's verify envelope"); + bool verified = await envelope.verify(); + if (verified) { + print("ENVELOPE PLAIN ${envelope.envelope.hasPlain()}"); + print("ENVELOPE Cipher ${envelope.envelope.hasCipher()}"); + + if (envelope.envelope.hasPlain()) { + final dataReceived = envelope.plain!; + if (dataReceived.isNotEmpty) { + try { + final decodedData = utf8.decode(dataReceived); + final parsedResponse = jsonDecode(decodedData); + responses.remove(requestID); + print("Parsed Response $parsedResponse"); + } catch (error) { + print('Error processing data: $error'); + rethrow; + } + } + } else if (envelope.envelope.hasCipher()) { + print(String.fromCharCodes(twin!.pk as Iterable)); + final decryptedCipher = await envelope.decrypt(mnemonic); + } + } else if (!verified) { + print("Not Verified"); + responses.remove(requestID); + throw Exception("Invalid signature, discarding response"); + } + } + + if (envelope != null && envelope.envelope.hasError()) { + throw Exception("ENVELOPE ERROR: ${envelope.error!.message}"); + } + } + } + + Future createSigner() async { + if (keypairType == KPType.ed25519) { + signer = await KeyPair.ed25519.fromMnemonic(mnemonic); + } else if (keypairType == KPType.sr25519) { + signer = await KeyPair.sr25519.fromMnemonic(mnemonic); + } else { + throw UnsupportedError('Unsupported Keypair type'); + } + } + + // void closeConnection() { + // if (channel != null) { + // channel!.sink.close(); + // } + // } + + Future updateUrl() async { + final token = await newJWT(session); + return '$relayUrl?$token'; + } +} diff --git a/packages/rmb_client/lib/src/envelope.dart b/packages/rmb_client/lib/src/envelope.dart new file mode 100644 index 00000000..1351905d --- /dev/null +++ b/packages/rmb_client/lib/src/envelope.dart @@ -0,0 +1,267 @@ +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:bip39/bip39.dart'; +import 'package:crypto/crypto.dart'; +import 'package:encrypt/encrypt.dart'; +import 'package:encrypt/encrypt.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:pointycastle/export.dart'; +import 'package:polkadart_keyring/polkadart_keyring.dart'; +import 'package:rmb_client/rmb_client.dart'; +import 'package:rmb_client/src/sign.dart'; +import 'package:rmb_client/src/utils.dart'; +import 'package:rmb_client/types/generated/types.pb.dart'; +import 'package:tfchain_client/generated/dev/types/pallet_tfgrid/types/entity.dart'; +import 'package:tfchain_client/generated/dev/types/pallet_tfgrid/types/twin.dart'; +import 'package:tfchain_client/tfchain_client.dart' + as TFClient; // Adjust this import based on your project structure +import 'package:crypto/crypto.dart' as Crypto; +import 'dart:convert'; // For utf8.encode +import 'package:convert/convert.dart'; +import 'package:ss58/ss58.dart' as SS58; + +class ClientEnvelope { + late final Envelope envelope; + late KeyPair signer; + late final String chainUrl; + Twin? twin; + late final TFClient.QueryClient tfClient; + late final Map twins; + + ClientEnvelope({ + KeyPair? signer, + required Envelope envelope, + required this.chainUrl, + required this.tfClient, + required this.twins, + }) : envelope = envelope { + if (signer != null) { + this.signer = signer; + } + envelope.schema = "application/json"; + } + + String? get uid => envelope.uid; + String? get tags => envelope.tags; + Int64? get timestamp => envelope.timestamp; + Int64? get expiration => envelope.expiration; + Address? get source => envelope.source; + Address? get destination => envelope.destination; + Request? get request => envelope.request; + Response? get response => envelope.response; + List? get signature => envelope.signature; + String? get schema => envelope.schema; + String? get federation => envelope.federation; + Error? get error => envelope.error; + List? get plain => envelope.plain; + List? get cipher => envelope.cipher; + Ping? get ping => envelope.ping; + Pong? get pong => envelope.pong; + List? get relays => envelope.relays; + + set signature(List? value) { + envelope.signature = value!; + } + + set plain(List? value) { + envelope.plain = value!; + } + + set relays(List? value) { + relays = value; + } + + Uint8List signEnvelope() { + final toSign = challenge(); + return sign(toSign, signer); + } + + Future getSigner(KeyPairType type) async { + try { + // final keyring = Keyring(); + // final address = keyring.encodeAddress(twin!.accountId); + // print("Address $address"); + // // KeyPair.sr25519. + // //5CJrCjZvsudNoJApTGG5PKcZfhAzAyGqgSK8bysoCV2oRBMC + + // final x = SS58.Address.decode(address); + + // // signer = keyring. + // print(signer.address); + // print(signer.keyPairType); + + signer = await KeyPair.sr25519.fromMnemonic( + "example athlete word glove believe result relief click local motion team any"); + + sleep(Duration(seconds: 20)); + print("ADDRESS: ${signer.address}"); + } catch (e) { + print("Error retrieving signer: $e"); + } + } + + Uint8List createNonce(int size) { + Random random = Random(); + List randArr = + List.generate(size, (index) => (random.nextDouble() * 10).toInt()); + return Uint8List.fromList(randArr); + } + + Uint8List challenge() { + var sink = AccumulatorSink(); + var md5Hasher = md5.startChunkedConversion(sink); + + md5Hasher.add(utf8.encode(uid!)); + + md5Hasher.add(utf8.encode(tags!)); + + md5Hasher.add(utf8.encode('$timestamp')); + + md5Hasher.add(utf8.encode('$expiration')); + + md5Hasher.add(utf8.encode(challengeAddress(envelope.source))); + + md5Hasher.add(utf8.encode(challengeAddress(envelope.destination))); + + if (request != null) { + md5Hasher = challengeRequest(request!, md5Hasher); + } else if (response != null) { + md5Hasher = challengeResponse(response!, md5Hasher); + } else if (error != null) { + md5Hasher = challengeError(error!, md5Hasher); + } + if (schema!.isNotEmpty) { + md5Hasher.add(utf8.encode(schema!)); + } + + if (federation!.isEmpty) { + md5Hasher.add(utf8.encode(federation!)); + } + if (plain!.isNotEmpty) { + final plainHex = hex.encode(plain!); + md5Hasher.add(hex.decode(plainHex)); + } else if (cipher!.isNotEmpty) { + final plainHex = hex.encode(cipher!); + md5Hasher.add(hex.decode(plainHex)); + } + + if (relays!.isNotEmpty) { + for (String relay in relays!) { + md5Hasher.add(utf8.encode(relay)); + } + } + + md5Hasher.close(); + Crypto.Digest updatedHash = sink.events.single; + + return Uint8List.fromList(updatedHash.bytes); + } + + String challengeAddress(Address address) { + return '${address.twin}${address.connection}'; + } + + ByteConversionSink challengeRequest( + Request request, ByteConversionSink md5Hasher) { + md5Hasher.add(utf8.encode(request.command)); + return md5Hasher; + } + + ByteConversionSink challengeError(Error err, ByteConversionSink md5Hasher) { + md5Hasher.add(utf8.encode('${error!.code}${error!.message}')); + return md5Hasher; + } + + ByteConversionSink challengeResponse( + Response response, ByteConversionSink md5Hasher) { + return md5Hasher; + } + + Future encrypt( + dynamic requestData, String mnemonic, String destTwinPk) async { + final publicKey = Uint8List.fromList(hexStringToArrayBuffer(destTwinPk)); + } + + Future decrypt(String mnemonic) async { + final publicKey = Uint8List.fromList(twin!.pk!); + final sharedKey = await createShared(publicKey, mnemonic); + final iv = cipher!.sublist(0, 12); + final slicedData = cipher!.sublist(12); + + // Create AES-GCM encrypter + final key = Key(sharedKey); + final initializationVector = IV(Uint8List.fromList(iv)); + final decrypter = Encrypter(AES(key, mode: AESMode.gcm)); + final decrypted = decrypter.decryptBytes( + Encrypted(Uint8List.fromList(slicedData)), + iv: initializationVector, + ); + + final decryptedText = utf8.decode(decrypted); + print("DECRYPTED TEXT"); + print(decryptedText); + } + + Future verify() async { + try { + String sig = String.fromCharCodes(signature!); + + final prefix = sig.substring(0, 1); + KeyPairType? sigType; + + if (prefix == 'e') { + sigType = KeyPairType.ed25519; + } else if (prefix == 's') { + sigType = KeyPairType.sr25519; + } else { + return false; + } + + print(sigType); + + // get twin of sender from twinid + twin = await getTwin(source!.twin, twins, tfClient); + print(twin!.id); + // print(signer.address); + await getSigner(sigType); + // print(signer.address); + final dataHashed = challenge(); + try { + signer.verify(Uint8List.fromList(dataHashed), + Uint8List.fromList(signature!.sublist(1))); + } catch (error) { + throw Exception(error); + } + return signer.verify( + dataHashed, Uint8List.fromList(signature!.sublist(1))); + } catch (error) { + print("Invalid Destination Twin: $error"); + return false; + } + } + + Future createShared( + Uint8List pubKey, String hexSeedOrMnemonic) async { + List privateKey; + + if (hexSeedOrMnemonic.length == 66) { + privateKey = + Uint8List.fromList((hexSeedOrMnemonic.substring(2).codeUnits)); + } else if (hexSeedOrMnemonic.length == 64) { + privateKey = Uint8List.fromList(hexSeedOrMnemonic.codeUnits); + } else if (validateMnemonic(hexSeedOrMnemonic)) { + final seed = mnemonicToSeed(hexSeedOrMnemonic); + privateKey = seed.sublist(0, 32); + } else { + throw Exception( + 'Expected a valid mnemonic or hexSeed in "createShared" but got: $hexSeedOrMnemonic.'); + } + + final pointX = + await getSharedSecret(Uint8List.fromList(privateKey), pubKey); + final sha256Digest = SHA256Digest(); + return sha256Digest.process(Uint8List.fromList(pointX).sublist(1, 33)); + } +} diff --git a/packages/rmb_client/lib/src/sign.dart b/packages/rmb_client/lib/src/sign.dart new file mode 100644 index 00000000..f72973fc --- /dev/null +++ b/packages/rmb_client/lib/src/sign.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:pointycastle/ecc/ecc_fp.dart'; +import 'package:pointycastle/export.dart'; +import 'package:polkadart_keyring/polkadart_keyring.dart' show KeyPair; +import 'package:cryptography/cryptography.dart' as crypto; +import 'package:pointycastle/ecc/api.dart' as api; + +class KPType { + static const sr25519 = "sr25519"; + static const ed25519 = "ed25519"; +} + +Uint8List sign(Uint8List payload, KeyPair signer) { + // remove last element from payload list + // final newPayload = Uint8List.fromList(payload.sublist(0, payload.length - 1)); + + // String resultString = String.fromCharCodes(newPayload); + String typePrefix = ""; + if (signer.keyPairType.name == KPType.ed25519) { + typePrefix = "e"; + } else if (signer.keyPairType.name == KPType.sr25519) { + typePrefix = "s"; + } + + final sig = signer.sign(payload); + + Uint8List uint8List = Uint8List.fromList(utf8.encode(typePrefix)); + + int prefix = uint8List.elementAt(0); + + Uint8List sigPrefixed = Uint8List.fromList([prefix, ...sig]); + + return sigPrefixed; +} + +bool isProbPub(Uint8List item) { + return item.length == 33 || item.length == 65; +} + +Uint8List _bigIntToUint8List(BigInt number) { + var _byteArray = number.toRadixString(16).padLeft(64, '0'); + return Uint8List.fromList(hex.decode(_byteArray)); +} + +BigInt _uint8ListToBigInt(Uint8List uint8List) { + return BigInt.parse(hex.encode(uint8List), radix: 16); +} + +// ECPrivateKey normalizePrivateKey(Uint8List privateKey) { +// final d = BigInt.parse(hex.encode(privateKey), radix: 16); +// final params = ECDomainParameters('secp256r1'); +// return ECPrivateKey(d, params); +// } + +// ECPublicKey normalizePublicKey(Uint8List publicKey, api.ECCurve curve) { +// final q = curve.decodePoint(publicKey); +// if (q == null) { +// throw ArgumentError('Invalid public key'); +// } +// final params = ECDomainParameters('secp256r1'); +// return ECPublicKey(q, params); +// } + +/// Normalize scalar value. +// BigInt normalizeScalar(BigInt scalar) { +// final domainParams = ECCurve_secp256r1(); +// final curveOrder = domainParams.n; +// if (scalar >= BigInt.zero && scalar < curveOrder) { +// return scalar; +// } else { +// return scalar % curveOrder; +// } +// } + +/// Perform point multiplication on an elliptic curve. +// api.ECPoint multiply(BigInt scalar, api.ECPoint affinePoint, +// {bool useEndomorphism = false}) { +// final n = normalizeScalar(scalar); +// api.ECPoint point, fake; + +// if (useEndomorphism) { +// final splitScalars = splitScalarEndo(n); +// final k1 = splitScalars['k1']!; +// final k2 = splitScalars['k2']!; +// final k1p = affinePoint * k1; +// final k2p = affinePoint * k2; + +// point = k1p + k2p; +// } else { +// point = affinePoint * n; +// } + +// return point.normalize(); +// } + +Future> getSharedSecret(Uint8List privateA, Uint8List publicB) async { + try { + print('Starting getSharedSecret function'); + // final curve = ECCurve_secp256r1(); + // final domainParams = ECDomainParameters('secp256r1'); + + // if (!isProbPub(privateA)) { + // throw ArgumentError('getSharedSecret: first arg must be private key'); + // } + // if (!isProbPub(publicB)) { + // throw ArgumentError('getSharedSecret: second arg must be public key'); + // } + + // final normalizedPrivateA = normalizePrivateKey(privateA); + // final normalizedPublicB = normalizePublicKey(publicB, domainParams.curve); + + // // Multiply the keys + // final sharedSecret = normalizedPublicB * normalizedPrivateA.d; + + // return toRawBytes(sharedSecret, isCompressed); + // final sharedSecret = normalizedPublicB * normalizedPrivateA.d; + + // return toRawBytes(sharedSecret, isCompressed); + + final params = ECCurve_secp256r1(); + final curve = params.curve; + print('Curve and parameters initialized'); + + // Create private key parameters + final dA = _uint8ListToBigInt(privateA); + final privateKey = ECPrivateKey(dA, params); + print('Private key created'); + + print('Public key (hex): ${hex.encode(publicB)}'); + print('Public key length: ${publicB.length}'); + + final Q = curve.decodePoint(publicB); + if (Q == null) { + throw ArgumentError('Failed to decode public key'); + } + print('Public key decoded'); + final publicKeyParams = ECPublicKey(Q, params); + + final keyAgreement = ECDHBasicAgreement(); + keyAgreement.init(privateKey); + + final sharedSecret = keyAgreement.calculateAgreement(publicKeyParams); + print('Shared secret calculated'); + + final sharedSecretBytes = _bigIntToUint8List(sharedSecret); + print('Shared Secret (hex): ${hex.encode(sharedSecretBytes)}'); + return sharedSecretBytes; + } catch (error) { + throw Exception('Error computing shared secret: $error'); + } +} + +// Future> getSharedSecret(Uint8List privateA, Uint8List publicB) async { +// try { +// print('Starting getSharedSecret function'); +// final algorithm = crypto.Ecdh.p256(length: 256); + +// print('Algorithm initialized'); + +// final localKeyPair = crypto.SimpleKeyPairData(privateA, +// publicKey: +// crypto.SimplePublicKey(publicB, type: crypto.KeyPairType.p256), +// type: crypto.KeyPairType.p256); +// print('Local key pair created: $localKeyPair'); + +// final remotePublicKey = +// crypto.SimplePublicKey(publicB, type: crypto.KeyPairType.p256); +// print('Remote public key created: $remotePublicKey'); + +// final sharedSecretKey = await algorithm.sharedSecretKey( +// keyPair: localKeyPair, remotePublicKey: remotePublicKey); +// print('SharedSecretKey computed'); +// final res = await sharedSecretKey.extractBytes(); +// return res; +// } catch (error) { +// throw Exception('Error computing shared secret: $error'); +// } +// } diff --git a/packages/rmb_client/lib/src/utils.dart b/packages/rmb_client/lib/src/utils.dart new file mode 100644 index 00000000..68652fde --- /dev/null +++ b/packages/rmb_client/lib/src/utils.dart @@ -0,0 +1,62 @@ +import 'dart:typed_data'; + +import 'package:rmb_client/rmb_client.dart'; +import 'package:tfchain_client/generated/dev/types/pallet_tfgrid/types/twin.dart'; +import 'package:tfchain_client/models/twins.dart'; +import 'package:tfchain_client/tfchain_client.dart' as TFClient; + +Future getTwin( + int id, Map twins, TFClient.QueryClient tfclient) async { + TwinsMap? mappedTwin = twins[id]; + bool isValid = false; + + if (mappedTwin != null) { + DateTime timestamp = + DateTime.fromMillisecondsSinceEpoch(mappedTwin.timestamp * 1000); + DateTime validTime = DateTime.now().subtract(Duration(minutes: 10)); + isValid = timestamp.isAfter(validTime); + } + + Twin? twin; + if (mappedTwin != null && isValid) { + twin = mappedTwin.twin; + } else { + twin = await tfclient.twins.get(QueryTwinsGetOptions(id: id)); + twins[id] = + TwinsMap(twin!, (DateTime.now().millisecondsSinceEpoch / 1000).round()); + } + + return twin; +} + +Uint8List hexStringToArrayBuffer(String hexString) { + // remove the leading 0x + hexString = hexString.replaceFirst(RegExp(r'^0x'), ''); + + // ensure even number of characters + if (hexString.length % 2 != 0) { + print("WARNING: expecting an even number of characters in the hexString"); + } + + // check for some non-hex characters + final bad = RegExp(r'[^0-9A-Fa-f]').allMatches(hexString); + if (bad.isNotEmpty) { + print("WARNING: found non-hex characters"); + for (var match in bad) { + print("Non-hex character: ${match.group(0)} at position ${match.start}"); + } + } + + // split the string into pairs of octets + final pairs = RegExp(r'.{1,2}') + .allMatches(hexString) + .map((match) => match.group(0)) + .where((s) => s != null) + .map((s) => s!) + .toList(); + + // convert the octets to integers + final integers = pairs.map((s) => int.parse(s, radix: 16)).toList(); + + return Uint8List.fromList(integers); +} diff --git a/packages/rmb_client/lib/types/generated/types.pb.dart b/packages/rmb_client/lib/types/generated/types.pb.dart new file mode 100644 index 00000000..b7618558 --- /dev/null +++ b/packages/rmb_client/lib/types/generated/types.pb.dart @@ -0,0 +1,638 @@ +// +// Generated code. Do not modify. +// source: types.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +/// A Request annotate this message as a request message +/// with proper command +class Request extends $pb.GeneratedMessage { + factory Request({ + $core.String? command, + }) { + final $result = create(); + if (command != null) { + $result.command = command; + } + return $result; + } + Request._() : super(); + factory Request.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Request.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Request', createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'command') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Request clone() => Request()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Request copyWith(void Function(Request) updates) => super.copyWith((message) => updates(message as Request)) as Request; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Request create() => Request._(); + Request createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Request getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Request? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get command => $_getSZ(0); + @$pb.TagNumber(1) + set command($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasCommand() => $_has(0); + @$pb.TagNumber(1) + void clearCommand() => clearField(1); +} + +/// A Response annotate this message as a response message +class Response extends $pb.GeneratedMessage { + factory Response() => create(); + Response._() : super(); + factory Response.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Response.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Response', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Response clone() => Response()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Response copyWith(void Function(Response) updates) => super.copyWith((message) => updates(message as Response)) as Response; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response create() => Response._(); + Response createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Response getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Response? _defaultInstance; +} + +/// A Error annotiate this message as an error message +class Error extends $pb.GeneratedMessage { + factory Error({ + $core.int? code, + $core.String? message, + }) { + final $result = create(); + if (code != null) { + $result.code = code; + } + if (message != null) { + $result.message = message; + } + return $result; + } + Error._() : super(); + factory Error.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Error.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Error', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'code', $pb.PbFieldType.OU3) + ..aOS(2, _omitFieldNames ? '' : 'message') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Error clone() => Error()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Error copyWith(void Function(Error) updates) => super.copyWith((message) => updates(message as Error)) as Error; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Error create() => Error._(); + Error createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Error getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Error? _defaultInstance; + + /// error code (app specific) + @$pb.TagNumber(1) + $core.int get code => $_getIZ(0); + @$pb.TagNumber(1) + set code($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasCode() => $_has(0); + @$pb.TagNumber(1) + void clearCode() => clearField(1); + + /// error message + @$pb.TagNumber(2) + $core.String get message => $_getSZ(1); + @$pb.TagNumber(2) + set message($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasMessage() => $_has(1); + @$pb.TagNumber(2) + void clearMessage() => clearField(2); +} + +class Address extends $pb.GeneratedMessage { + factory Address({ + $core.int? twin, + $core.String? connection, + }) { + final $result = create(); + if (twin != null) { + $result.twin = twin; + } + if (connection != null) { + $result.connection = connection; + } + return $result; + } + Address._() : super(); + factory Address.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Address.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Address', createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'twin', $pb.PbFieldType.OU3) + ..aOS(2, _omitFieldNames ? '' : 'connection') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Address clone() => Address()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Address copyWith(void Function(Address) updates) => super.copyWith((message) => updates(message as Address)) as Address; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Address create() => Address._(); + Address createEmptyInstance() => create(); + static $pb.PbList
createRepeated() => $pb.PbList
(); + @$core.pragma('dart2js:noInline') + static Address getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor
(create); + static Address? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get twin => $_getIZ(0); + @$pb.TagNumber(1) + set twin($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasTwin() => $_has(0); + @$pb.TagNumber(1) + void clearTwin() => clearField(1); + + @$pb.TagNumber(2) + $core.String get connection => $_getSZ(1); + @$pb.TagNumber(2) + set connection($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasConnection() => $_has(1); + @$pb.TagNumber(2) + void clearConnection() => clearField(2); +} + +/// an app level ping pong +/// in case you are using javascript +/// and cant send ping messages +/// when sending Pings, both signature +/// and destination are ignored +class Ping extends $pb.GeneratedMessage { + factory Ping() => create(); + Ping._() : super(); + factory Ping.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Ping.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Ping', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Ping clone() => Ping()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Ping copyWith(void Function(Ping) updates) => super.copyWith((message) => updates(message as Ping)) as Ping; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Ping create() => Ping._(); + Ping createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Ping getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Ping? _defaultInstance; +} + +/// if the relay received an envelope ping, +/// an envelope pong will be sent back to the +/// client +class Pong extends $pb.GeneratedMessage { + factory Pong() => create(); + Pong._() : super(); + factory Pong.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Pong.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Pong', createEmptyInstance: create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Pong clone() => Pong()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Pong copyWith(void Function(Pong) updates) => super.copyWith((message) => updates(message as Pong)) as Pong; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Pong create() => Pong._(); + Pong createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Pong getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Pong? _defaultInstance; +} + +enum Envelope_Message { + request, + response, + error, + ping, + pong, + notSet +} + +enum Envelope_Payload { + plain, + cipher, + notSet +} + +class Envelope extends $pb.GeneratedMessage { + factory Envelope({ + $core.String? uid, + $core.String? tags, + $fixnum.Int64? timestamp, + $fixnum.Int64? expiration, + Address? source, + Address? destination, + Request? request, + Response? response, + $core.List<$core.int>? signature, + $core.String? schema, + $core.String? federation, + Error? error, + $core.List<$core.int>? plain, + $core.List<$core.int>? cipher, + Ping? ping, + Pong? pong, + $core.Iterable<$core.String>? relays, + }) { + final $result = create(); + if (uid != null) { + $result.uid = uid; + } + if (tags != null) { + $result.tags = tags; + } + if (timestamp != null) { + $result.timestamp = timestamp; + } + if (expiration != null) { + $result.expiration = expiration; + } + if (source != null) { + $result.source = source; + } + if (destination != null) { + $result.destination = destination; + } + if (request != null) { + $result.request = request; + } + if (response != null) { + $result.response = response; + } + if (signature != null) { + $result.signature = signature; + } + if (schema != null) { + $result.schema = schema; + } + if (federation != null) { + $result.federation = federation; + } + if (error != null) { + $result.error = error; + } + if (plain != null) { + $result.plain = plain; + } + if (cipher != null) { + $result.cipher = cipher; + } + if (ping != null) { + $result.ping = ping; + } + if (pong != null) { + $result.pong = pong; + } + if (relays != null) { + $result.relays.addAll(relays); + } + return $result; + } + Envelope._() : super(); + factory Envelope.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Envelope.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, Envelope_Message> _Envelope_MessageByTag = { + 7 : Envelope_Message.request, + 8 : Envelope_Message.response, + 12 : Envelope_Message.error, + 15 : Envelope_Message.ping, + 16 : Envelope_Message.pong, + 0 : Envelope_Message.notSet + }; + static const $core.Map<$core.int, Envelope_Payload> _Envelope_PayloadByTag = { + 13 : Envelope_Payload.plain, + 14 : Envelope_Payload.cipher, + 0 : Envelope_Payload.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Envelope', createEmptyInstance: create) + ..oo(0, [7, 8, 12, 15, 16]) + ..oo(1, [13, 14]) + ..aOS(1, _omitFieldNames ? '' : 'uid') + ..aOS(2, _omitFieldNames ? '' : 'tags') + ..a<$fixnum.Int64>(3, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..a<$fixnum.Int64>(4, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOM
(5, _omitFieldNames ? '' : 'source', subBuilder: Address.create) + ..aOM
(6, _omitFieldNames ? '' : 'destination', subBuilder: Address.create) + ..aOM(7, _omitFieldNames ? '' : 'request', subBuilder: Request.create) + ..aOM(8, _omitFieldNames ? '' : 'response', subBuilder: Response.create) + ..a<$core.List<$core.int>>(9, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY) + ..aOS(10, _omitFieldNames ? '' : 'schema') + ..aOS(11, _omitFieldNames ? '' : 'federation') + ..aOM(12, _omitFieldNames ? '' : 'error', subBuilder: Error.create) + ..a<$core.List<$core.int>>(13, _omitFieldNames ? '' : 'plain', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(14, _omitFieldNames ? '' : 'cipher', $pb.PbFieldType.OY) + ..aOM(15, _omitFieldNames ? '' : 'ping', subBuilder: Ping.create) + ..aOM(16, _omitFieldNames ? '' : 'pong', subBuilder: Pong.create) + ..pPS(17, _omitFieldNames ? '' : 'relays') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Envelope clone() => Envelope()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Envelope copyWith(void Function(Envelope) updates) => super.copyWith((message) => updates(message as Envelope)) as Envelope; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Envelope create() => Envelope._(); + Envelope createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Envelope getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Envelope? _defaultInstance; + + Envelope_Message whichMessage() => _Envelope_MessageByTag[$_whichOneof(0)]!; + void clearMessage() => clearField($_whichOneof(0)); + + Envelope_Payload whichPayload() => _Envelope_PayloadByTag[$_whichOneof(1)]!; + void clearPayload() => clearField($_whichOneof(1)); + + /// uid is auto generated by rmb. + @$pb.TagNumber(1) + $core.String get uid => $_getSZ(0); + @$pb.TagNumber(1) + set uid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUid() => $_has(0); + @$pb.TagNumber(1) + void clearUid() => clearField(1); + + /// client specific tags + @$pb.TagNumber(2) + $core.String get tags => $_getSZ(1); + @$pb.TagNumber(2) + set tags($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasTags() => $_has(1); + @$pb.TagNumber(2) + void clearTags() => clearField(2); + + /// timestamp of sending the envlope + @$pb.TagNumber(3) + $fixnum.Int64 get timestamp => $_getI64(2); + @$pb.TagNumber(3) + set timestamp($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasTimestamp() => $_has(2); + @$pb.TagNumber(3) + void clearTimestamp() => clearField(3); + + /// message TTL from the time of send + @$pb.TagNumber(4) + $fixnum.Int64 get expiration => $_getI64(3); + @$pb.TagNumber(4) + set expiration($fixnum.Int64 v) { $_setInt64(3, v); } + @$pb.TagNumber(4) + $core.bool hasExpiration() => $_has(3); + @$pb.TagNumber(4) + void clearExpiration() => clearField(4); + + /// sender id + @$pb.TagNumber(5) + Address get source => $_getN(4); + @$pb.TagNumber(5) + set source(Address v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasSource() => $_has(4); + @$pb.TagNumber(5) + void clearSource() => clearField(5); + @$pb.TagNumber(5) + Address ensureSource() => $_ensure(4); + + /// destination of the envlope + @$pb.TagNumber(6) + Address get destination => $_getN(5); + @$pb.TagNumber(6) + set destination(Address v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasDestination() => $_has(5); + @$pb.TagNumber(6) + void clearDestination() => clearField(6); + @$pb.TagNumber(6) + Address ensureDestination() => $_ensure(5); + + @$pb.TagNumber(7) + Request get request => $_getN(6); + @$pb.TagNumber(7) + set request(Request v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasRequest() => $_has(6); + @$pb.TagNumber(7) + void clearRequest() => clearField(7); + @$pb.TagNumber(7) + Request ensureRequest() => $_ensure(6); + + @$pb.TagNumber(8) + Response get response => $_getN(7); + @$pb.TagNumber(8) + set response(Response v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasResponse() => $_has(7); + @$pb.TagNumber(8) + void clearResponse() => clearField(8); + @$pb.TagNumber(8) + Response ensureResponse() => $_ensure(7); + + /// signature + @$pb.TagNumber(9) + $core.List<$core.int> get signature => $_getN(8); + @$pb.TagNumber(9) + set signature($core.List<$core.int> v) { $_setBytes(8, v); } + @$pb.TagNumber(9) + $core.bool hasSignature() => $_has(8); + @$pb.TagNumber(9) + void clearSignature() => clearField(9); + + /// schema of the payload of either the request or the resposne message. + @$pb.TagNumber(10) + $core.String get schema => $_getSZ(9); + @$pb.TagNumber(10) + set schema($core.String v) { $_setString(9, v); } + @$pb.TagNumber(10) + $core.bool hasSchema() => $_has(9); + @$pb.TagNumber(10) + void clearSchema() => clearField(10); + + /// a federation url (domain) + /// if not provided the relay assumes it's a local twin + /// but if provided it can be checked against the relay + /// domain, and hence decided if message need federation + /// or local. + @$pb.TagNumber(11) + $core.String get federation => $_getSZ(10); + @$pb.TagNumber(11) + set federation($core.String v) { $_setString(10, v); } + @$pb.TagNumber(11) + $core.bool hasFederation() => $_has(10); + @$pb.TagNumber(11) + void clearFederation() => clearField(11); + + @$pb.TagNumber(12) + Error get error => $_getN(11); + @$pb.TagNumber(12) + set error(Error v) { setField(12, v); } + @$pb.TagNumber(12) + $core.bool hasError() => $_has(11); + @$pb.TagNumber(12) + void clearError() => clearField(12); + @$pb.TagNumber(12) + Error ensureError() => $_ensure(11); + + @$pb.TagNumber(13) + $core.List<$core.int> get plain => $_getN(12); + @$pb.TagNumber(13) + set plain($core.List<$core.int> v) { $_setBytes(12, v); } + @$pb.TagNumber(13) + $core.bool hasPlain() => $_has(12); + @$pb.TagNumber(13) + void clearPlain() => clearField(13); + + @$pb.TagNumber(14) + $core.List<$core.int> get cipher => $_getN(13); + @$pb.TagNumber(14) + set cipher($core.List<$core.int> v) { $_setBytes(13, v); } + @$pb.TagNumber(14) + $core.bool hasCipher() => $_has(13); + @$pb.TagNumber(14) + void clearCipher() => clearField(14); + + @$pb.TagNumber(15) + Ping get ping => $_getN(14); + @$pb.TagNumber(15) + set ping(Ping v) { setField(15, v); } + @$pb.TagNumber(15) + $core.bool hasPing() => $_has(14); + @$pb.TagNumber(15) + void clearPing() => clearField(15); + @$pb.TagNumber(15) + Ping ensurePing() => $_ensure(14); + + @$pb.TagNumber(16) + Pong get pong => $_getN(15); + @$pb.TagNumber(16) + set pong(Pong v) { setField(16, v); } + @$pb.TagNumber(16) + $core.bool hasPong() => $_has(15); + @$pb.TagNumber(16) + void clearPong() => clearField(16); + @$pb.TagNumber(16) + Pong ensurePong() => $_ensure(15); + + @$pb.TagNumber(17) + $core.List<$core.String> get relays => $_getList(16); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/packages/rmb_client/lib/types/generated/types.pbenum.dart b/packages/rmb_client/lib/types/generated/types.pbenum.dart new file mode 100644 index 00000000..9d235e4b --- /dev/null +++ b/packages/rmb_client/lib/types/generated/types.pbenum.dart @@ -0,0 +1,11 @@ +// +// Generated code. Do not modify. +// source: types.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + diff --git a/packages/rmb_client/lib/types/generated/types.pbjson.dart b/packages/rmb_client/lib/types/generated/types.pbjson.dart new file mode 100644 index 00000000..9ff46a27 --- /dev/null +++ b/packages/rmb_client/lib/types/generated/types.pbjson.dart @@ -0,0 +1,137 @@ +// +// Generated code. Do not modify. +// source: types.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use requestDescriptor instead') +const Request$json = { + '1': 'Request', + '2': [ + {'1': 'command', '3': 1, '4': 1, '5': 9, '10': 'command'}, + ], + '9': [ + {'1': 2, '2': 3}, + ], +}; + +/// Descriptor for `Request`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List requestDescriptor = $convert.base64Decode( + 'CgdSZXF1ZXN0EhgKB2NvbW1hbmQYASABKAlSB2NvbW1hbmRKBAgCEAM='); + +@$core.Deprecated('Use responseDescriptor instead') +const Response$json = { + '1': 'Response', + '9': [ + {'1': 1, '2': 2}, + {'1': 2, '2': 3}, + ], +}; + +/// Descriptor for `Response`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( + 'CghSZXNwb25zZUoECAEQAkoECAIQAw=='); + +@$core.Deprecated('Use errorDescriptor instead') +const Error$json = { + '1': 'Error', + '2': [ + {'1': 'code', '3': 1, '4': 1, '5': 13, '10': 'code'}, + {'1': 'message', '3': 2, '4': 1, '5': 9, '10': 'message'}, + ], +}; + +/// Descriptor for `Error`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List errorDescriptor = $convert.base64Decode( + 'CgVFcnJvchISCgRjb2RlGAEgASgNUgRjb2RlEhgKB21lc3NhZ2UYAiABKAlSB21lc3NhZ2U='); + +@$core.Deprecated('Use addressDescriptor instead') +const Address$json = { + '1': 'Address', + '2': [ + {'1': 'twin', '3': 1, '4': 1, '5': 13, '10': 'twin'}, + {'1': 'connection', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'connection', '17': true}, + ], + '8': [ + {'1': '_connection'}, + ], +}; + +/// Descriptor for `Address`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List addressDescriptor = $convert.base64Decode( + 'CgdBZGRyZXNzEhIKBHR3aW4YASABKA1SBHR3aW4SIwoKY29ubmVjdGlvbhgCIAEoCUgAUgpjb2' + '5uZWN0aW9uiAEBQg0KC19jb25uZWN0aW9u'); + +@$core.Deprecated('Use pingDescriptor instead') +const Ping$json = { + '1': 'Ping', +}; + +/// Descriptor for `Ping`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pingDescriptor = $convert.base64Decode( + 'CgRQaW5n'); + +@$core.Deprecated('Use pongDescriptor instead') +const Pong$json = { + '1': 'Pong', +}; + +/// Descriptor for `Pong`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pongDescriptor = $convert.base64Decode( + 'CgRQb25n'); + +@$core.Deprecated('Use envelopeDescriptor instead') +const Envelope$json = { + '1': 'Envelope', + '2': [ + {'1': 'uid', '3': 1, '4': 1, '5': 9, '10': 'uid'}, + {'1': 'tags', '3': 2, '4': 1, '5': 9, '9': 2, '10': 'tags', '17': true}, + {'1': 'timestamp', '3': 3, '4': 1, '5': 4, '10': 'timestamp'}, + {'1': 'expiration', '3': 4, '4': 1, '5': 4, '10': 'expiration'}, + {'1': 'source', '3': 5, '4': 1, '5': 11, '6': '.Address', '10': 'source'}, + {'1': 'destination', '3': 6, '4': 1, '5': 11, '6': '.Address', '10': 'destination'}, + {'1': 'request', '3': 7, '4': 1, '5': 11, '6': '.Request', '9': 0, '10': 'request'}, + {'1': 'response', '3': 8, '4': 1, '5': 11, '6': '.Response', '9': 0, '10': 'response'}, + {'1': 'error', '3': 12, '4': 1, '5': 11, '6': '.Error', '9': 0, '10': 'error'}, + {'1': 'ping', '3': 15, '4': 1, '5': 11, '6': '.Ping', '9': 0, '10': 'ping'}, + {'1': 'pong', '3': 16, '4': 1, '5': 11, '6': '.Pong', '9': 0, '10': 'pong'}, + {'1': 'signature', '3': 9, '4': 1, '5': 12, '9': 3, '10': 'signature', '17': true}, + {'1': 'schema', '3': 10, '4': 1, '5': 9, '9': 4, '10': 'schema', '17': true}, + {'1': 'federation', '3': 11, '4': 1, '5': 9, '9': 5, '10': 'federation', '17': true}, + {'1': 'plain', '3': 13, '4': 1, '5': 12, '9': 1, '10': 'plain'}, + {'1': 'cipher', '3': 14, '4': 1, '5': 12, '9': 1, '10': 'cipher'}, + {'1': 'relays', '3': 17, '4': 3, '5': 9, '10': 'relays'}, + ], + '8': [ + {'1': 'message'}, + {'1': 'payload'}, + {'1': '_tags'}, + {'1': '_signature'}, + {'1': '_schema'}, + {'1': '_federation'}, + ], +}; + +/// Descriptor for `Envelope`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List envelopeDescriptor = $convert.base64Decode( + 'CghFbnZlbG9wZRIQCgN1aWQYASABKAlSA3VpZBIXCgR0YWdzGAIgASgJSAJSBHRhZ3OIAQESHA' + 'oJdGltZXN0YW1wGAMgASgEUgl0aW1lc3RhbXASHgoKZXhwaXJhdGlvbhgEIAEoBFIKZXhwaXJh' + 'dGlvbhIgCgZzb3VyY2UYBSABKAsyCC5BZGRyZXNzUgZzb3VyY2USKgoLZGVzdGluYXRpb24YBi' + 'ABKAsyCC5BZGRyZXNzUgtkZXN0aW5hdGlvbhIkCgdyZXF1ZXN0GAcgASgLMgguUmVxdWVzdEgA' + 'UgdyZXF1ZXN0EicKCHJlc3BvbnNlGAggASgLMgkuUmVzcG9uc2VIAFIIcmVzcG9uc2USHgoFZX' + 'Jyb3IYDCABKAsyBi5FcnJvckgAUgVlcnJvchIbCgRwaW5nGA8gASgLMgUuUGluZ0gAUgRwaW5n' + 'EhsKBHBvbmcYECABKAsyBS5Qb25nSABSBHBvbmcSIQoJc2lnbmF0dXJlGAkgASgMSANSCXNpZ2' + '5hdHVyZYgBARIbCgZzY2hlbWEYCiABKAlIBFIGc2NoZW1hiAEBEiMKCmZlZGVyYXRpb24YCyAB' + 'KAlIBVIKZmVkZXJhdGlvbogBARIWCgVwbGFpbhgNIAEoDEgBUgVwbGFpbhIYCgZjaXBoZXIYDi' + 'ABKAxIAVIGY2lwaGVyEhYKBnJlbGF5cxgRIAMoCVIGcmVsYXlzQgkKB21lc3NhZ2VCCQoHcGF5' + 'bG9hZEIHCgVfdGFnc0IMCgpfc2lnbmF0dXJlQgkKB19zY2hlbWFCDQoLX2ZlZGVyYXRpb24='); + diff --git a/packages/rmb_client/lib/types/generated/types.pbserver.dart b/packages/rmb_client/lib/types/generated/types.pbserver.dart new file mode 100644 index 00000000..ad376413 --- /dev/null +++ b/packages/rmb_client/lib/types/generated/types.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: types.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'types.pb.dart'; + diff --git a/packages/rmb_client/lib/types/types.proto b/packages/rmb_client/lib/types/types.proto new file mode 100644 index 00000000..9c59cbce --- /dev/null +++ b/packages/rmb_client/lib/types/types.proto @@ -0,0 +1,78 @@ +syntax = "proto3"; + +// A Request annotate this message as a request message +// with proper command +message Request { + reserved 2; + string command = 1; +} + +// A Response annotate this message as a response message +message Response { reserved 1, 2; } + +// A Error annotiate this message as an error message +message Error { + // error code (app specific) + uint32 code = 1; + // error message + string message = 2; +} + +message Address { + uint32 twin = 1; + optional string connection = 2; +} + +// an app level ping pong +// in case you are using javascript +// and cant send ping messages +// when sending Pings, both signature +// and destination are ignored +message Ping {} +// if the relay received an envelope ping, +// an envelope pong will be sent back to the +// client +message Pong {} + +message Envelope { + // uid is auto generated by rmb. + string uid = 1; + // client specific tags + optional string tags = 2; + // timestamp of sending the envlope + uint64 timestamp = 3; + // message TTL from the time of send + uint64 expiration = 4; + // sender id + Address source = 5; + // destination of the envlope + Address destination = 6; + // message inside the envlope + oneof message { + Request request = 7; + Response response = 8; + Error error = 12; + Ping ping = 15; + Pong pong = 16; + } + // signature + optional bytes signature = 9; + + // schema of the payload of either the request or the resposne message. + optional string schema = 10; + + // a federation url (domain) + // if not provided the relay assumes it's a local twin + // but if provided it can be checked against the relay + // domain, and hence decided if message need federation + // or local. + optional string federation = 11; + + // pyload of the message is interpreted differently based + // on the message filed + oneof payload { + bytes plain = 13; + bytes cipher = 14; + } + repeated string relays = 17; +} \ No newline at end of file diff --git a/packages/rmb_client/pubspec.yaml b/packages/rmb_client/pubspec.yaml new file mode 100644 index 00000000..c3a511d8 --- /dev/null +++ b/packages/rmb_client/pubspec.yaml @@ -0,0 +1,31 @@ +name: rmb_client +description: A sample command-line application. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo +publish_to: none + +environment: + sdk: ^3.2.0 + +# Add regular dependencies here. +dependencies: + fixnum: ^1.1.0 + polkadart_keyring: ^0.4.3 + protobuf: ^3.1.0 + web_socket_channel: ^2.4.1 + tfchain_client: + path: ../tfchain_client + uuid: ^4.4.0 + async_locks: ^4.0.0 + crypto: ^3.0.3 + convert: ^3.1.1 + secp256k1: ^0.3.0 + pointycastle: ^3.9.1 + cryptography: ^2.7.0 + ss58: ^1.1.2 + encrypt: ^5.0.3 + bip39: ^1.0.6 + +dev_dependencies: + lints: ^4.0.0 + test: ^1.24.0 diff --git a/packages/rmb_client/test/rmb_client_test.dart b/packages/rmb_client/test/rmb_client_test.dart new file mode 100644 index 00000000..e69de29b diff --git a/packages/tfchain_client/lib/tfchain_client.dart b/packages/tfchain_client/lib/tfchain_client.dart index b355def7..0644545e 100644 --- a/packages/tfchain_client/lib/tfchain_client.dart +++ b/packages/tfchain_client/lib/tfchain_client.dart @@ -22,3 +22,4 @@ import 'package:convert/convert.dart'; import 'package:signer/signer.dart' as Signer; part 'src/client.dart'; + diff --git a/pubspec.lock b/pubspec.lock index 55e3779e..c6aa1174 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -273,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hashlib: + dependency: transitive + description: + name: hashlib + sha256: "67e640e19cc33070113acab3125cd48ebe480a0300e15554dec089b8878a729f" + url: "https://pub.dev" + source: hosted + version: "1.16.0" hashlib_codecs: dependency: transitive description: @@ -521,6 +529,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" pub_semver: dependency: transitive description: @@ -577,6 +593,13 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + rmb_client: + dependency: "direct main" + description: + path: "packages/rmb_client" + relative: true + source: path + version: "1.0.0" secp256k1_ecdsa: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3aee7fd2..2a35fb9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,7 @@ name: tfgrid_sdk_dart description: A sample command-line application. version: 1.0.0 +publish_to: none environment: sdk: ^3.2.0 @@ -16,5 +17,6 @@ dependencies: path: ./packages/signer substrate_bip39: ^0.4.0 tfchain_client: - path: ./packages/tfchain_client - + path: ./packages/tfchain_client + rmb_client: + path: ./packages/rmb_client