diff --git a/CHANGELOG.md b/CHANGELOG.md index be97b35..dd0eb2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [2.0.0] + +* Dependencies update. +* Added Get Balance. +* Added Transfer Balance. +* Added Delegate an Account. + # [1.0.1] * Dependencies update. diff --git a/README.md b/README.md index 120952a..8dc5f0c 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,14 @@ Tezos is a decentralized blockchain that governs itself by establishing a true d ### Features * Tezos wallet utilities. + * Get Balance. * Generate mnemonics. * Generate keys from mnemonic. * Generate keys from mnemonics and passphrase. * Sign Operation Group. * Unlock fundraiser identity. + * Transfer Balance. + * Delegate an Account. ### Getting started @@ -32,6 +35,12 @@ import 'package:tezster_dart/tezster_dart.dart'; ### Usage +* Get Balance + +``` dart +String balance = await TezsterDart.getBalance('tz1c....ozGGs', 'your rpc server'); +``` + * Generate mnemonic ``` dart @@ -86,6 +95,65 @@ List identityFundraiser = await TezsterDart.unlockFundraiserIdentity( tz1hhkSbaocSWm3wawZUuUdX57L3maSH16Pv] */ ``` +* Transfer Balance. + * The most basic operation on the chain is the transfer of value between two accounts. In this example we have the account we activated above: tz1QSHaKpTFhgHLbqinyYRjxD5sLcbfbzhxy and some random testnet address to test with: tz1RVcUP9nUurgEJMDou8eW3bVDs6qmP5Lnc. Note all amounts are in µtz, as in micro-tez, hence 0.5tz is represented as 500000. The fee of 1500 was chosen arbitrarily, but some operations have minimum fee requirements. + +``` dart +var server = ''; + +var keyStore = KeyStoreModel( + publicKey: 'edpkvQtuhdZQmjdjVfaY9Kf4hHfrRJYugaJErkCGvV3ER1S7XWsrrj', + secretKey: + 'edskRgu8wHxjwayvnmpLDDijzD3VZDoAH7ZLqJWuG4zg7LbxmSWZWhtkSyM5Uby41rGfsBGk4iPKWHSDniFyCRv3j7YFCknyHH', + publicKeyHash: 'tz1QSHaKpTFhgHLbqinyYRjxD5sLcbfbzhxy', + ); + +var signer = await TezsterDart.createSigner( + TezsterDart.writeKeyWithHint(keyStore.secretKey, 'edsk')); + +var result = await TezsterDart.sendTransactionOperation( + server, + signer, + keyStore, + 'tz1RVcUP9nUurgEJMDou8eW3bVDs6qmP5Lnc', + 500000, + 1500, + ); + +print("Applied operation ===> $result['appliedOp']"); +print("Operation groupID ===> $result['operationGroupID']"); + +``` + +* Delegate an Account. + * One of the most exciting features of Tezos is delegation. This is a means for non-"baker" (non-validator) accounts to participate in the on-chain governance process and receive staking rewards. It is possible to delegate both implicit and originated accounts. For implicit addresses, those starting with tz1, tz2 and tz3, simply call sendDelegationOperation. Originated accounts, that is smart contracts, must explicitly support delegate assignment, but can also be deployed with a delegate already set. + +``` dart +var server = ''; + +var keyStore = KeyStoreModel( + publicKey: 'edpkvQtuhdZQmjdjVfaY9Kf4hHfrRJYugaJErkCGvV3ER1S7XWsrrj', + secretKey: + 'edskRgu8wHxjwayvnmpLDDijzD3VZDoAH7ZLqJWuG4zg7LbxmSWZWhtkSyM5Uby41rGfsBGk4iPKWHSDniFyCRv3j7YFCknyHH', + publicKeyHash: 'tz1QSHaKpTFhgHLbqinyYRjxD5sLcbfbzhxy', + ); + +var signer = await TezsterDart.createSigner( + TezsterDart.writeKeyWithHint(keyStore.secretKey, 'edsk')); + +var result = await TezsterDart.sendDelegationOperation( + server, + signer, + keyStore, + 'tz1RVcUP9nUurgEJMDou8eW3bVDs6qmP5Lnc', + 10000, + ); + +print("Applied operation ===> $result['appliedOp']"); +print("Operation groupID ===> $result['operationGroupID']"); + +``` + --- **NOTE:** Use stable version of flutter to avoid package conflicts. diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 55ca830..583c0ce 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ In most cases you can leave this as-is, but you if you want to provide additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + { print("identityFundraiser ===> $identityFundraiser"); //identityFundraiser ===> [privateKey, publicKey, publicKeyHash] //Accessing: private key ===> identityFundraiser[0] | public key ===> identityFundraiser[1] | public Key Hash ===> identityFundraiser[2] all of type string. + + // Get Balance + String balance = + await TezsterDart.getBalance('tz1c....ozGGs', 'your rpc server'); + print("Accoutn Balance ===> $balance"); + + var server = ''; + + var keyStore = KeyStoreModel( + publicKey: 'edpkvQtuhdZQmjdjVfaY9Kf4hHfrRJYugaJErkCGvV3ER1S7XWsrrj', + secretKey: + 'edskRgu8wHxjwayvnmpLDDijzD3VZDoAH7ZLqJWuG4zg7LbxmSWZWhtkSyM5Uby41rGfsBGk4iPKWHSDniFyCRv3j7YFCknyHH', + publicKeyHash: 'tz1QSHaKpTFhgHLbqinyYRjxD5sLcbfbzhxy', + ); + + //Send transaction + var transactionSigner = await TezsterDart.createSigner( + TezsterDart.writeKeyWithHint(keyStore.secretKey, 'edsk')); + var transactionResult = await TezsterDart.sendTransactionOperation( + server, + transactionSigner, + keyStore, + 'tz1RVcUP9nUurgEJMDou8eW3bVDs6qmP5Lnc', + 500000, + 1500, + ); + print("Applied operation ===> $transactionResult['appliedOp']"); + print("Operation groupID ===> $transactionResult['operationGroupID']"); + + //Send delegation + var delegationSigner = await TezsterDart.createSigner( + TezsterDart.writeKeyWithHint(keyStore.secretKey, 'edsk')); + var delegationResult = await TezsterDart.sendDelegationOperation( + server, + delegationSigner, + keyStore, + 'tz1RVcUP9nUurgEJMDou8eW3bVDs6qmP5Lnc', + 10000, + ); + print("Applied operation ===> $delegationResult['appliedOp']"); + print("Operation groupID ===> $delegationResult['operationGroupID']"); } @override diff --git a/example/pubspec.lock b/example/pubspec.lock index 26dec30..ad80921 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -218,7 +218,7 @@ packages: path: ".." relative: true source: path - version: "1.0.1" + version: "2.0.0" typed_data: dependency: transitive description: diff --git a/lib/chain/tezos/tezos_message_codec.dart b/lib/chain/tezos/tezos_message_codec.dart new file mode 100644 index 0000000..58464cb --- /dev/null +++ b/lib/chain/tezos/tezos_message_codec.dart @@ -0,0 +1,50 @@ +import 'package:tezster_dart/chain/tezos/tezos_message_utils.dart'; +import 'package:tezster_dart/models/operation_model.dart'; + +class TezosMessageCodec { + static String encodeOperation(OperationModel message) { + if (message.kind == 'transaction') return encodeTransaction(message); + if (message.kind == 'reveal') return encodeReveal(message); + if (message.kind == 'delegation') return encodeDelegation(message); + } + + static String encodeTransaction(OperationModel message) { + String hex = TezosMessageUtils.writeInt(108); + hex += TezosMessageUtils.writeAddress(message.source).substring(2); + hex += TezosMessageUtils.writeInt(int.parse(message.fee)); + hex += TezosMessageUtils.writeInt(message.counter); + hex += TezosMessageUtils.writeInt(message.gasLimit); + hex += TezosMessageUtils.writeInt(message.storageLimit); + hex += TezosMessageUtils.writeInt(int.parse(message.amount)); + hex += TezosMessageUtils.writeAddress(message.destination); + hex += '00'; + return hex; + } + + static String encodeReveal(OperationModel message) { + var hex = TezosMessageUtils.writeInt(107); //sepyTnoitarepo['reveal']); + hex += TezosMessageUtils.writeAddress(message.source).substring(2); + hex += TezosMessageUtils.writeInt(int.parse(message.fee)); + hex += TezosMessageUtils.writeInt(message.counter); + hex += TezosMessageUtils.writeInt(message.gasLimit); + hex += TezosMessageUtils.writeInt(message.storageLimit); + hex += TezosMessageUtils.writePublicKey(message.publicKey); + return hex; + } + + static String encodeDelegation(OperationModel delegation) { + var hex = TezosMessageUtils.writeInt(110); + hex += TezosMessageUtils.writeAddress(delegation.source).substring(2); + hex += TezosMessageUtils.writeInt(int.parse(delegation.fee)); + hex += TezosMessageUtils.writeInt(delegation.counter); + hex += TezosMessageUtils.writeInt(delegation.gasLimit); + hex += TezosMessageUtils.writeInt(delegation.storageLimit); + if (delegation.delegate != null && delegation.delegate.isNotEmpty) { + hex += TezosMessageUtils.writeBoolean(true); + hex += TezosMessageUtils.writeAddress(delegation.delegate).substring(2); + } else { + hex += TezosMessageUtils.writeBoolean(false); + } + return hex; + } +} diff --git a/lib/chain/tezos/tezos_message_utils.dart b/lib/chain/tezos/tezos_message_utils.dart new file mode 100644 index 0000000..c6d13c4 --- /dev/null +++ b/lib/chain/tezos/tezos_message_utils.dart @@ -0,0 +1,104 @@ +import 'dart:typed_data'; + +import 'package:blake2b/blake2b_hash.dart'; +import 'package:bs58check/bs58check.dart'; +import 'package:convert/convert.dart'; +import 'package:tezster_dart/helper/generateKeys.dart'; +import 'package:tezster_dart/src/soft-signer/soft_signer.dart'; + +class TezosMessageUtils { + static String writeBranch(String branch) { + return hex.encode(base58 + .decode(branch) + .sublist(2, base58.decode(branch).length - 4) + .toList()); + // return hex.encode(base58.decode(branch).sublist(2).toList()); + } + + static String writeInt(int value) { + if (value < 0) { + throw new Exception('Use writeSignedInt to encode negative numbers'); + } + var byteHexList = Uint8List.fromList(hex.decode(twoByteHex(value))); + for (var i = 0; i < byteHexList.length; i++) { + if (i != 0) byteHexList[i] ^= 0x80; + } + var result = hex.encode(byteHexList.reversed.toList()); + return result; + } + + static String twoByteHex(int n) { + if (n < 128) { + var s = ('0' + n.toRadixString(16)); + return s.substring(s.length - 2); + } + String h = ''; + if (n > 2147483648) { + var r = BigInt.from(n); + while (r.compareTo(BigInt.zero) != -1) { + var _h = ('0' + (r & BigInt.from(127)).toRadixString(16)); + h = _h.substring(_h.length - 2) + h; + r = r >> 7; + } + } else { + var r = n; + while (r > 0) { + var _h = ('0' + (r & 127).toRadixString(16)); + h = _h.substring(_h.length - 2) + h; + r = r >> 7; + } + } + return h; + } + + static String writeAddress(String address) { + var base58data = base58.decode(address).sublist(3); + base58data = base58data.sublist(0, base58data.length - 4); + var _hex = hex.encode(base58data); + if (address.startsWith("tz1")) { + return "0000" + _hex; + } else if (address.startsWith("tz2")) { + return "0001" + _hex; + } else if (address.startsWith("tz3")) { + return "0002" + _hex; + } else if (address.startsWith("KT1")) { + return "01" + _hex + "00"; + } else { + throw new Exception( + 'Unrecognized address prefix: ${address.substring(0, 3)}'); + } + } + + static String writePublicKey(String publicKey) { + if (publicKey.startsWith("edpk")) { + return "00" + hex.encode(base58.decode(publicKey).sublist(4)); + } else if (publicKey.startsWith("sppk")) { + return "01" + hex.encode(base58.decode(publicKey).sublist(4)); + } else if (publicKey.startsWith("p2pk")) { + return "02" + hex.encode(base58.decode(publicKey).sublist(4)); + } else { + throw new Exception('Unrecognized key type'); + } + } + + static Uint8List simpleHash(Uint8List message, int size) { + return Uint8List.fromList(Blake2bHash.hashWithDigestSize(256, message)); + } + + static String readSignatureWithHint(Uint8List opSignature, SignerCurve hint) { + opSignature = Uint8List.fromList(opSignature); + if (hint == SignerCurve.ED25519) { + return GenerateKeys.readKeysWithHint(opSignature, '09f5cd8612'); + } else if (hint == SignerCurve.SECP256K1) { + return GenerateKeys.readKeysWithHint(opSignature, '0d7365133f'); + } else if (hint == SignerCurve.SECP256R1) { + return GenerateKeys.readKeysWithHint(opSignature, '36f02c34'); + } else { + throw Exception('Unrecognized signature hint, "$hint"'); + } + } + + static String writeBoolean(bool b) { + return b ? "ff" : "00"; + } +} diff --git a/lib/chain/tezos/tezos_node_reader.dart b/lib/chain/tezos/tezos_node_reader.dart new file mode 100644 index 0000000..909bf8b --- /dev/null +++ b/lib/chain/tezos/tezos_node_reader.dart @@ -0,0 +1,45 @@ + +import 'package:tezster_dart/helper/http_helper.dart'; + +class TezosNodeReader { + static Future getCounterForAccount(String server, String publicKeyHash, + {String chainid = 'main'}) async { + var response = await HttpHelper.performGetRequest(server, + 'chains/$chainid/blocks/head/context/contracts/$publicKeyHash/counter'); + return int.parse(response.toString().replaceAll('"', ''), radix: 10); + } + + static Future isManagerKeyRevealedForAccount( + String server, String publicKeyHash) async { + var managerKey = + await getAccountManagerForBlock(server, 'head', publicKeyHash); + return managerKey.length > 0; + } + + static Future getAccountManagerForBlock( + String server, String block, String publicKeyHash, + {String chainid = 'main'}) async { + var response = await HttpHelper.performGetRequest(server, + 'chains/$chainid/blocks/$block/context/contracts/$publicKeyHash/manager_key'); + return response.toString().isNotEmpty ? response.toString() : ''; + } + + static Future> getBlockAtOffset( + String server, int offset, + {String chainid = 'main'}) async { + if (offset <= 0) { + return await getBlock(server); + } + var head = await getBlock(server); + var response = await HttpHelper.performGetRequest( + server, 'chains/$chainid/blocks/${head['header']['level'] - offset}'); + return response; + } + + static Future> getBlock(String server, + {String hash = 'head', String chainid = 'main'}) async { + var response = await HttpHelper.performGetRequest( + server, 'chains/$chainid/blocks/$hash'); + return response; + } +} diff --git a/lib/chain/tezos/tezos_node_writer.dart b/lib/chain/tezos/tezos_node_writer.dart new file mode 100644 index 0000000..bb680d8 --- /dev/null +++ b/lib/chain/tezos/tezos_node_writer.dart @@ -0,0 +1,188 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:tezster_dart/chain/tezos/tezos_message_codec.dart'; +import 'package:tezster_dart/chain/tezos/tezos_message_utils.dart'; +import 'package:tezster_dart/chain/tezos/tezos_node_reader.dart'; +import 'package:tezster_dart/helper/constants.dart'; +import 'package:tezster_dart/helper/http_helper.dart'; +import 'package:tezster_dart/models/key_store_model.dart'; +import 'package:tezster_dart/models/operation_model.dart'; +import 'package:tezster_dart/src/soft-signer/soft_signer.dart'; + +class TezosNodeWriter { + static Future> sendTransactionOperation(String server, + SoftSigner signer, KeyStoreModel keyStore, String to, int amount, int fee, + {int offset = 54}) async { + var counter = await TezosNodeReader.getCounterForAccount( + server, keyStore.publicKeyHash) + + 1; + + OperationModel transaction = new OperationModel( + destination: to, + amount: amount.toString(), + counter: counter, + fee: fee.toString(), + source: keyStore.publicKeyHash, + ); + + var operations = await appendRevealOperation(server, keyStore.publicKey, + keyStore.publicKeyHash, counter - 1, [transaction]); + return sendOperation(server, operations, signer, offset); + } + + static sendDelegationOperation(String server, SoftSigner signer, + KeyStoreModel keyStore, String delegate, int fee, offset) async { + var counter = await TezosNodeReader.getCounterForAccount( + server, keyStore.publicKeyHash) + + 1; + OperationModel delegation = new OperationModel( + counter: counter, + kind: 'delegation', + fee: fee.toString(), + source: keyStore.publicKeyHash, + delegate: delegate, + ); + + var operations = await appendRevealOperation(server, keyStore.publicKey, + keyStore.publicKeyHash, counter - 1, [delegation]); + return sendOperation(server, operations, signer, offset); + } + + static Future> appendRevealOperation( + String server, + String publicKey, + String publicKeyHash, + int accountOperationIndex, + List operations) async { + bool isKeyRevealed = await TezosNodeReader.isManagerKeyRevealedForAccount( + server, publicKeyHash); + var counter = accountOperationIndex + 1; + if (!isKeyRevealed) { + var revealOp = OperationModel( + counter: counter, + fee: '0', + source: publicKeyHash, + kind: 'reveal', + gasLimit: 10600, + storageLimit: 0, + publicKey: publicKey, + ); + for (var index = 0; index < operations.length; index++) { + var c = accountOperationIndex + 2 + index; + operations[index].counter = c; + } + return [revealOp, ...operations]; + } + return operations; + } + + static Future> sendOperation(String server, + List operations, SoftSigner signer, int offset) async { + var blockHead = await TezosNodeReader.getBlockAtOffset(server, offset); + var blockHash = blockHead['hash'].toString().substring(0, 51); + var forgedOperationGroup = forgeOperations(blockHash, operations); + var opSignature = signer.signOperation(Uint8List.fromList(hex.decode( + TezosConstants.OperationGroupWatermark + forgedOperationGroup))); + var signedOpGroup = Uint8List.fromList( + hex.decode(forgedOperationGroup) + opSignature.toList()); + var base58signature = TezosMessageUtils.readSignatureWithHint( + opSignature, signer.getSignerCurve()); + var opPair = {'bytes': signedOpGroup, 'signature': base58signature}; + var appliedOp = await preapplyOperation( + server, blockHash, blockHead['protocol'], operations, opPair); + var injectedOperation = await injectOperation(server, opPair); + + return {'appliedOp': appliedOp[0], 'operationGroupID': injectedOperation}; + } + + static String forgeOperations( + String branch, List operations) { + var encoded = TezosMessageUtils.writeBranch(branch); + operations.forEach((element) { + encoded += TezosMessageCodec.encodeOperation(element); + }); + return encoded; + } + + static preapplyOperation(String server, String branch, protocol, + List operations, Map signedOpGroup, + {String chainid = 'main'}) async { + var payload = [ + { + 'protocol': protocol, + 'branch': branch, + 'contents': operations, + 'signature': signedOpGroup['signature'] + } + ]; + print("signedOpGroup['signature'] ===> ${signedOpGroup['signature']}"); + print("parameters ===> ${jsonEncode(payload)}"); + var response = await HttpHelper.performPostRequest(server, + 'chains/$chainid/blocks/head/helpers/preapply/operations', payload); + var json; + try { + json = jsonDecode(response); + } catch (err) { + throw new Exception( + 'Could not parse JSON from response of chains/$chainid/blocks/head/helpers/preapply/operation: $response for $payload'); + } + parseRPCError(jsonDecode(response)); + return json; + } + + static void parseRPCError(json) { + var errors = ''; + try { + var arr = json is List ? json : [json]; + if (json[0]['kind'] != null) { + errors = arr.fold( + '', + (previousValue, element) => + '$previousValue${element['kind']} : ${element['id']}, '); + } else if (arr.length == 1 && + arr[0]['contents'].length == 1 && + arr[0]['contents'][0]['kind'].toString() == "activate_account") {} + } catch (err) { + if (json.toString().startsWith('Failed to parse the request body: ')) { + errors = json.toString().toString().substring(34); + } else { + var hash = json + .toString() + .replaceFirst('/\"/g', "'") + .replaceFirst('/\n/', "'"); + if (hash.length == 51 && hash[0] == 'o') { + } else { + print( + "failed to parse errors: '$err' from '${json.toString()}'\n, PLEASE report this to the maintainers"); + } + } + } + + if (errors.length > 0) { + print('errors found in response:\n$json'); + throw Exception( + "Status code ==> 200\nResponse ==> $json \n Error ==> $errors"); + } + } + + static String parseRPCOperationResult(result) { + if (result.status == 'failed') { + return "${result.status}: ${result.errors.map((e) => '(${e.kind}: ${e.id})').join(', ')}"; + } else if (result.status == 'applied') { + return ''; + } else { + return result.status; + } + } + + static injectOperation(String server, Map opPair, + {chainid = 'main'}) async { + var response = await HttpHelper.performPostRequest(server, + 'injection/operation?chain=$chainid', hex.encode(opPair['bytes'])); + response = response.toString().replaceAll('"', ''); + // parseRPCError(response); + return response; + } +} diff --git a/lib/helper/constants.dart b/lib/helper/constants.dart new file mode 100644 index 0000000..e214d01 --- /dev/null +++ b/lib/helper/constants.dart @@ -0,0 +1,8 @@ +class TezosConstants { + static const OperationGroupWatermark = "03"; + static const DefaultTransactionStorageLimit = 496; + static const DefaultTransactionGasLimit = 10600; + static const DefaultDelegationFee = 1258; + static const DefaultDelegationStorageLimit = 0; + static const DefaultDelegationGasLimit = 1101; +} diff --git a/lib/helper/generateKeys.dart b/lib/helper/generateKeys.dart index 68dd391..64e83cb 100644 --- a/lib/helper/generateKeys.dart +++ b/lib/helper/generateKeys.dart @@ -28,4 +28,19 @@ class GenerateKeys { String base58String = bs58check.encode(convertingHexStringToListOfInt); return base58String; } + + static Uint8List writeKeyWithHint( + String key, + String hint, + ) { + if (hint == 'edsk' || + hint == 'edpk' || + hint == 'sppk' || + hint == 'p2pk' || + hint == '2bf64e07' || + hint == '0d0f25d9') { + return bs58check.decode(key).sublist(4); + } else + throw Exception("Unrecognized key hint, '$hint'"); + } } diff --git a/lib/helper/http_helper.dart b/lib/helper/http_helper.dart new file mode 100644 index 0000000..0ac5e9f --- /dev/null +++ b/lib/helper/http_helper.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'dart:io'; + +class HttpHelper { + static Future performPostRequest(server, command, payload) async { + HttpClient httpClient = new HttpClient(); + HttpClientRequest request = + await httpClient.postUrl(Uri.parse('$server/$command')); + request.headers.set('content-type', 'application/json'); + request.add(utf8.encode(json.encode(payload))); + HttpClientResponse response = await request.close(); + String reply = await response.transform(utf8.decoder).join(); + httpClient.close(); + return reply; + } + + static Future performGetRequest(server, command) async { + HttpClient httpClient = new HttpClient(); + HttpClientRequest request = + await httpClient.getUrl(Uri.parse('$server/$command')); + HttpClientResponse response = await request.close(); + String reply = await response.transform(utf8.decoder).join(); + httpClient.close(); + return jsonDecode(reply); + } +} diff --git a/lib/helper/password_generater.dart b/lib/helper/password_generater.dart new file mode 100644 index 0000000..4421459 --- /dev/null +++ b/lib/helper/password_generater.dart @@ -0,0 +1,48 @@ +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; + +class PasswordGenerator { + /// @desc Function to generate password based on some criteria + /// @param bool isWithLetters: password must contain letters + /// @param bool isWithUppercase: password must contain uppercase letters + /// @param bool isWithNumbers: password must contain numbers + /// @param bool isWithSpecial: password must contain special chars + /// @param int length: password length + /// @return string: new password + static String generatePassword( + {@required double length, + bool isWithLetters, + bool isWithUppercase, + bool isWithNumbers, + bool isWithSpecial}) { + //Define the allowed chars to use in the password + String _lowerCaseLetters = "abcdefghijklmnopqrstuvwxyz"; + String _upperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + String _numbers = "0123456789"; + String _special = r'!@#$%^&*()+_-=}{[]|:;"/?.><,`~'; + + //Create the empty string that will contain the allowed chars + String _allowedChars = ""; + + //Put chars on the allowed ones based on the input values + _allowedChars += (isWithLetters ? _lowerCaseLetters : ''); + _allowedChars += (isWithUppercase ? _upperCaseLetters : ''); + _allowedChars += (isWithNumbers ? _numbers : ''); + _allowedChars += (isWithSpecial ? _special : ''); + + int i = 0; + String _result = ""; + + //Create password + while (i < length.round()) { + //Get random int + int randomInt = Random.secure().nextInt(_allowedChars.length); + //Get random char and append it to the password + _result += _allowedChars[randomInt]; + i++; + } + + return _result; + } +} diff --git a/lib/models/key_store_model.dart b/lib/models/key_store_model.dart new file mode 100644 index 0000000..d00940c --- /dev/null +++ b/lib/models/key_store_model.dart @@ -0,0 +1,14 @@ +import 'package:flutter/cupertino.dart'; + +class KeyStoreModel { + String publicKey; + String secretKey; + String publicKeyHash; + String seed; + + KeyStoreModel( + {@required this.publicKey, + @required this.secretKey, + @required this.publicKeyHash, + this.seed}); +} diff --git a/lib/models/operation_model.dart b/lib/models/operation_model.dart new file mode 100644 index 0000000..516f157 --- /dev/null +++ b/lib/models/operation_model.dart @@ -0,0 +1,54 @@ +import 'package:tezster_dart/helper/constants.dart'; + +class OperationModel { + String destination; + String amount; + int storageLimit; + int gasLimit; + int counter; + String fee; + String source; + String kind; + String publicKey; + String delegate; + + OperationModel({ + this.destination, + this.amount, + this.counter, + this.fee, + this.source, + this.kind = 'transaction', + this.gasLimit = TezosConstants.DefaultTransactionGasLimit, + this.storageLimit = TezosConstants.DefaultTransactionStorageLimit, + this.publicKey, + this.delegate, + }) { + if (kind == 'delegation') { + gasLimit = TezosConstants.DefaultDelegationGasLimit; + storageLimit = TezosConstants.DefaultDelegationStorageLimit; + } + } + + Map toJson() => kind == 'delegation' + ? { + 'counter': counter.toString(), + 'delegate': delegate, + 'fee': fee.toString(), + 'gas_limit': TezosConstants.DefaultDelegationGasLimit.toString(), + 'kind': kind, + 'source': source, + 'storage_limit': + TezosConstants.DefaultDelegationStorageLimit.toString(), + } + : { + 'destination': destination, + 'amount': amount, + 'storage_limit': storageLimit.toString(), + 'gas_limit': gasLimit.toString(), + 'counter': counter.toString(), + 'fee': fee, + 'source': source, + 'kind': kind, + }; +} diff --git a/lib/src/soft-signer/soft_signer.dart b/lib/src/soft-signer/soft_signer.dart new file mode 100644 index 0000000..b848bb0 --- /dev/null +++ b/lib/src/soft-signer/soft_signer.dart @@ -0,0 +1,81 @@ +import 'dart:typed_data'; + +import 'package:tezster_dart/chain/tezos/tezos_message_utils.dart'; +import 'package:tezster_dart/helper/password_generater.dart'; +import 'package:tezster_dart/utils/crypto_utils.dart'; + +enum SignerCurve { ED25519, SECP256K1, SECP256R1 } + +class SoftSigner { + var _secretKey; + var _lockTimout; + var _passphrase; + var _salt; + var _unlocked; + var _key; + + SoftSigner( + {Uint8List secretKey, + int validity = -1, + String passphrase = '', + Uint8List salt}) { + this._secretKey = secretKey; + this._lockTimout = validity; + this._passphrase = passphrase; + this._salt = salt != null ? salt : Uint8List(0); + this._unlocked = validity < 0; + this._key = Uint8List(0); + if (validity < 0) { + this._key = secretKey; + } + } + + static SoftSigner createSigner(Uint8List secretKey, int validity) { + if (validity >= 0) { + var passphrase = PasswordGenerator.generatePassword( + length: 32, + isWithLetters: true, + isWithNumbers: true, + isWithSpecial: true, + isWithUppercase: true, + ); + var salt = CryptoUtils.generateSaltForPwHash(); + secretKey = CryptoUtils.encryptMessage(secretKey, passphrase, salt); + return SoftSigner( + secretKey: secretKey, + validity: validity, + passphrase: passphrase, + salt: salt, + ); + } else + return new SoftSigner(secretKey: secretKey); + } + + Uint8List getKey() { + if (!_unlocked) { + var k = CryptoUtils.decryptMessage(_secretKey, _passphrase, _salt); + if (_lockTimout == 0) { + return k; + } + _key = k; + _unlocked = true; + if (_lockTimout > 0) { + Future.delayed(Duration(milliseconds: _lockTimout * 1000), () { + _key = Uint8List(0); + _unlocked = false; + }); + } + return _key; + } + return _key; + } + + Uint8List signOperation(Uint8List uint8list) { + return CryptoUtils.signDetached(TezosMessageUtils.simpleHash(uint8list, 32), + Uint8List.fromList(getKey())); + } + + SignerCurve getSignerCurve() { + return SignerCurve.ED25519; + } +} diff --git a/lib/src/tezster_dart.dart b/lib/src/tezster_dart.dart index ed978a8..82c5ba3 100644 --- a/lib/src/tezster_dart.dart +++ b/lib/src/tezster_dart.dart @@ -10,6 +10,11 @@ import 'package:crypto/crypto.dart'; import 'package:password_hash/password_hash.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:bs58check/bs58check.dart' as bs58check; +import 'package:tezster_dart/chain/tezos/tezos_node_writer.dart'; +import 'package:tezster_dart/helper/constants.dart'; +import 'package:tezster_dart/helper/http_helper.dart'; +import 'package:tezster_dart/src/soft-signer/soft_signer.dart'; +import 'package:tezster_dart/tezster_dart.dart'; import "package:unorm_dart/unorm_dart.dart" as unorm; import 'package:flutter_sodium/flutter_sodium.dart'; @@ -112,4 +117,56 @@ class TezsterDart { String pkKeyHash = GenerateKeys.computeKeyHash(keyPair.pk); return [skKey, pkKey, pkKeyHash]; } + + static Future getBalance(String publicKeyHash, String rpc) async { + assert(publicKeyHash != null); + assert(rpc != null); + var response = await HttpHelper.performGetRequest(rpc, + 'chains/main/blocks/head/context/contracts/$publicKeyHash/balance'); + return response.toString(); + } + + static Uint8List writeKeyWithHint(key, hint) { + assert(key != null); + assert(hint != null); + return GenerateKeys.writeKeyWithHint(key, hint); + } + + static createSigner(Uint8List secretKey, {int validity = 60}) { + assert(secretKey != null); + return SoftSigner.createSigner(secretKey, validity); + } + + static sendTransactionOperation(String server, SoftSigner signer, + KeyStoreModel keyStore, String to, int amount, int fee, + {int offset = 54}) async { + assert(server != null); + assert(signer != null); + assert(keyStore != null); + assert(keyStore.publicKeyHash != null); + assert(keyStore.publicKey != null); + assert(keyStore.secretKey != null); + assert(to != null); + assert(amount != null); + assert(fee != null); + assert(offset != null); + + return await TezosNodeWriter.sendTransactionOperation( + server, signer, keyStore, to, amount, fee); + } + + static sendDelegationOperation(String server, SoftSigner signer, + KeyStoreModel keyStore, String delegate, int fee, + {offset = 54}) async { + assert(server != null); + assert(signer != null); + assert(keyStore != null); + assert(keyStore.publicKeyHash != null); + assert(keyStore.publicKey != null); + assert(keyStore.secretKey != null); + assert(offset != null); + if (fee == null || fee == 0) fee = TezosConstants.DefaultDelegationFee; + return await TezosNodeWriter.sendDelegationOperation( + server, signer, keyStore, delegate, fee, offset); + } } diff --git a/lib/tezster_dart.dart b/lib/tezster_dart.dart index 2e4ec75..a80299f 100644 --- a/lib/tezster_dart.dart +++ b/lib/tezster_dart.dart @@ -1,3 +1,6 @@ library tezster_dart; -export 'src/tezster_dart.dart'; \ No newline at end of file +export 'src/tezster_dart.dart'; + +// KeyStore Model +export 'models/key_store_model.dart'; diff --git a/lib/utils/crypto_utils.dart b/lib/utils/crypto_utils.dart new file mode 100644 index 0000000..12787c3 --- /dev/null +++ b/lib/utils/crypto_utils.dart @@ -0,0 +1,30 @@ +import 'dart:typed_data'; +import 'package:tezster_dart/utils/sodium_utils.dart'; + +class CryptoUtils { + static Uint8List generateSaltForPwHash() { + return SodiumUtils.salt(); + } + + static Uint8List encryptMessage( + Uint8List message, String passphrase, Uint8List salt) { + var keyBytes = SodiumUtils.pwhash(passphrase, salt); + var nonce = SodiumUtils.nonce(); + var s = SodiumUtils.close(message, nonce, keyBytes); + // nonce.addAll(s); + return new Uint8List.fromList(nonce.toList() + s.toList()); + } + + // static signDetached(Uint8List simpleHash) { + // return + // } + + static Uint8List decryptMessage(message, passphrase, salt) { + var keyBytes = SodiumUtils.pwhash(passphrase, salt); + return SodiumUtils.open(message, keyBytes); + } + + static Uint8List signDetached(Uint8List simpleHash, Uint8List key) { + return SodiumUtils.sign(simpleHash, key); + } +} diff --git a/lib/utils/sodium_utils.dart b/lib/utils/sodium_utils.dart new file mode 100644 index 0000000..64b65fb --- /dev/null +++ b/lib/utils/sodium_utils.dart @@ -0,0 +1,46 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:flutter_sodium/flutter_sodium.dart'; + +class SodiumUtils { + static Uint8List rand(length) { + return Sodium.randombytesBuf(length); + } + + static Uint8List salt() { + return Uint8List.fromList(rand(Sodium.cryptoPwhashSaltbytes).toList()); + } + + static Uint8List pwhash(String passphrase, Uint8List salt) { + return Sodium.cryptoPwhash( + Sodium.cryptoBoxSeedbytes, + Uint8List.fromList(passphrase.codeUnits), + salt, + 4, + 33554432, + Sodium.cryptoPwhashAlgArgon2i13); + } + + static Uint8List nonce() { + return rand(Sodium.cryptoBoxNoncebytes); + } + + static Uint8List close( + Uint8List message, Uint8List nonce, Uint8List keyBytes) { + return Sodium.cryptoSecretboxEasy(message, nonce, keyBytes); + } + + static Uint8List open(Uint8List nonce_and_ciphertext, Uint8List key) { + var nonce = + nonce_and_ciphertext.sublist(0, Sodium.cryptoSecretboxNoncebytes); + var ciphertext = + nonce_and_ciphertext.sublist(Sodium.cryptoSecretboxNoncebytes); + + return Sodium.cryptoSecretboxOpenEasy(ciphertext, nonce, key); + } + + static Uint8List sign(Uint8List simpleHash, Uint8List key) { + return Sodium.cryptoSignDetached(simpleHash, key); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 423891d..9029023 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tezster_dart description: A flutter package which provides the functionalities to play around with tezos dApps -version: 1.0.1 +version: 2.0.0 homepage: https://github.com/Tezsure/tezster_dart repository: https://github.com/Tezsure/tezster_dart issue_tracker: https://github.com/Tezsure/tezster_dart/issues diff --git a/test/tezos_test.dart b/test/tezos_test.dart index 445ebc9..ea4ec0d 100644 --- a/test/tezos_test.dart +++ b/test/tezos_test.dart @@ -42,4 +42,66 @@ void main() { expect(keys[1], "edpkvASxrq16v5Awxpz4XPTA2d6QFaCL8expPrPNcVgVbWxT84Kdw2"); expect(keys[2], "tz1hhkSbaocSWm3wawZUuUdX57L3maSH16Pv"); }); + + test('Create Soft Signer', () async { + var keyStore = KeyStoreModel( + publicKey: 'edpkuh9tUmMMVKJVqG4bJxNLsCob6y8wXycshi6Pn11SQ5hx7SAVjf', + secretKey: + 'edskRs9KBdoU675PBVyHdM3fqixemkykm7hgHeXAYKUjdoVn3Aev8dP11p47zc4iuWJsefSP4t2vdHPoQisQC3DjZY3ZbbSP9Y', + publicKeyHash: 'tz1LRibbLEEWpaXb4aKrXXgWPvx9ue9haAAV', + ); + + var signer = await TezsterDart.createSigner( + TezsterDart.writeKeyWithHint(keyStore.secretKey, 'edsk')); + }); + + test('send-Transaction-Operation', () async { + var keyStore = KeyStoreModel( + publicKey: 'edpkuh9tUmMMVKJVqG4bJxNLsCob6y8wXycshi6Pn11SQ5hx7SAVjf', + secretKey: + 'edskRs9KBdoU675PBVyHdM3fqixemkykm7hgHeXAYKUjdoVn3Aev8dP11p47zc4iuWJsefSP4t2vdHPoQisQC3DjZY3ZbbSP9Y', + publicKeyHash: 'tz1LRibbLEEWpaXb4aKrXXgWPvx9ue9haAAV', + ); + + var signer = await TezsterDart.createSigner( + TezsterDart.writeKeyWithHint(keyStore.secretKey, 'edsk')); + print(signer); + const server = 'https://testnet.tezster.tech'; + + var result = await TezsterDart.sendTransactionOperation( + server, + signer, + keyStore, + 'tz1LRibbLEEWpaXb4aKrXXgWPvx9ue9haAAV', + 500000, + 1500, + ); + expect(true, + result['operationGroupID'] != null && result['operationGroupID'] != ''); + }); + + test('send-Delegation-Operation', () async { + var keyStore = KeyStoreModel( + publicKey: 'edpkunM8fmwNb8NqcKZ1WiBrZQqvuN1NRY3FrSRer9HEySaPAkqgqt', + secretKey: + 'edskRjFXdYtHUrkLh7cs6b8EQigNi5uFGYxSsC3CgpvaA86Xcvo4TxrcmK155jY3c9hyxaQK8s8cfFXscEUFwdTjhFLf3P5LVX', + publicKeyHash: 'tz1csxCjsefVvKzWWAvhkoVn3M67wxaozGGs', + ); + + var signer = await TezsterDart.createSigner( + TezsterDart.writeKeyWithHint(keyStore.secretKey, 'edsk')); + print(signer); + const server = 'https://testnet.tezster.tech'; + + var result = await TezsterDart.sendDelegationOperation( + server, + signer, + keyStore, + 'tz1RUGhq8sQpfGu1W2kf7MixqWX7oxThBFLr', + 10000, + ); + + expect(true, + result['operationGroupID'] != null && result['operationGroupID'] != ''); + }); }