diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..187d14e --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +ENVIRONMENT = 'testnet' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5cfc5d2..a96b5db 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ doc/api/ dev/ **/.DS_Store + +.env diff --git a/example/recover_public_key_example.dart b/example/recover_public_key_example.dart new file mode 100644 index 0000000..79182ca --- /dev/null +++ b/example/recover_public_key_example.dart @@ -0,0 +1,30 @@ +import 'dart:typed_data'; + +import 'package:witnet/crypto.dart'; +import 'package:witnet/src/utils/transformations/transformations.dart'; +import 'package:witnet/witnet.dart'; + +// abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about +String xprvString = + "xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvu566tn6"; + +void main() { + // import a xprv + Xprv xprv = Xprv.fromXprv(xprvString); + var expectedKey = bytesToHex(xprv.privateKey.publicKey.point.encode()); + + // sign a message + String messageStr = "Hello Witnet!"; + Uint8List messageBytes = sha256(data: stringToBytes(messageStr)); + WitSignature signature = xprv.privateKey.signature(bytesToHex(messageBytes)); + + // recover a public key from a signature + WitPublicKey recoveredKey = WitPublicKey.recover(signature, messageBytes); + + try { + assert(expectedKey == bytesToHex(recoveredKey.point.encode()), + "error: Message not signed by expected Public Key"); + } catch (e) { + print(e); + } +} diff --git a/example/stake_transaction_example.dart b/example/stake_transaction_example.dart new file mode 100644 index 0000000..1d04ddd --- /dev/null +++ b/example/stake_transaction_example.dart @@ -0,0 +1,84 @@ +// import 'package:witnet/node.dart'; +import 'package:witnet/schema.dart'; +import 'package:witnet/src/constants.dart'; +import 'package:witnet/src/utils/transformations/transformations.dart'; +import 'package:witnet/witnet.dart'; + +var outputPointer = OutputPointer.fromString( + '0000000000000000000000000000000000000000000000000000000000000000:0'); + +void main() async { + /// connect to local node rpc + // NodeClient nodeClient = NodeClient(address: "127.0.0.1", port: 21338); + + // String mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + /// load node xprv for the default mnemonic + Xprv masterNode = Xprv.fromXprv( + "xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvu566tn6"); + + Xprv withdrawer = masterNode / + KEYPATH_PURPOSE / + KEYPATH_COIN_TYPE / + KEYPATH_ACCOUNT / + EXTERNAL_KEYCHAIN / + 0; + + /// The 20 byte Public Key Hash of the withdrawer + String pkh = bytesToHex(withdrawer.privateKey.publicKey.publicKeyHash); + + /// The authorization by the node + KeyedSignature authorization = signHash(pkh, masterNode.privateKey); + + /// Build the Stake Key + StakeKey stakeKey = StakeKey( + validator: authorization.publicKey.pkh, + withdrawer: PublicKeyHash.fromAddress(withdrawer.address.address), + ); + + /// build stake transaction body + StakeBody body = StakeBody( + inputs: [ + Input(outputPointer: outputPointer), + ], + output: StakeOutput( + value: MINIMUM_STAKEABLE_AMOUNT_WITS, + key: stakeKey, + authorization: authorization, + ), + ); + + /// build and sign stake transaction + StakeTransaction stake = StakeTransaction( + body: body, + signatures: [signHash(body.transactionId, masterNode.privateKey)]); + + /// The Stake Transaction ID + print(stake.transactionID); + + /// send stake transaction + /// var response = await nodeClient.inventory(stake.jsonMap()); + /// + UnstakeBody unstakeBody = UnstakeBody( + operator: PublicKeyHash.fromAddress(withdrawer.address.address), + withdrawal: ValueTransferOutput.fromJson({ + "pkh": withdrawer.address.address, + "time_lock": 0, + "value": 1, + })); + + KeyedSignature unstakeSignature = + signHash(bytesToHex(unstakeBody.hash), masterNode.privateKey); + UnstakeTransaction unstake = + UnstakeTransaction(body: unstakeBody, signature: unstakeSignature); + + print(unstake.transactionID); +} + +/// Sign Hash +KeyedSignature signHash(String hash, WitPrivateKey privateKey) { + final sig = privateKey.signature(hash); + return KeyedSignature( + publicKey: PublicKey(bytes: privateKey.publicKey.encode()), + signature: Signature(secp256k1: Secp256k1Signature(der: sig.encode())), + ); +} diff --git a/lib/constants.dart b/lib/constants.dart index a8cfdc0..8bdd107 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -12,4 +12,7 @@ export 'src/constants.dart' KEYPATH_PURPOSE, KEYPATH_COIN_TYPE, EXTERNAL_KEYCHAIN, - INTERNAL_KEYCHAIN; + INTERNAL_KEYCHAIN, + STAKE_OUTPUT_WEIGHT, + UNSTAKE_OUTPUT_WEIGHT, + MINIMUM_STAKEABLE_AMOUNT_WITS; diff --git a/lib/explorer.dart b/lib/explorer.dart index 1115eeb..85b35ec 100644 --- a/lib/explorer.dart +++ b/lib/explorer.dart @@ -47,4 +47,11 @@ export 'src/network/explorer/explorer_api.dart' TransactionType, TransactionUtxo, TxStatusLabel, - ValueTransferInfo; + ValueTransferInfo, + StakeInfo, + StakeInput, + AddressStakes, + AddressStake, + AddressUnstake, + AddressUnstakes, + UnstakeInfo; diff --git a/lib/src/constants.dart b/lib/src/constants.dart index d0b8be0..1a10d62 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,5 +1,8 @@ const INPUT_SIZE = 133; const OUTPUT_SIZE = 36; +const STAKE_OUTPUT_WEIGHT = 105; +const UNSTAKE_OUTPUT_WEIGHT = 153; +const MINIMUM_STAKEABLE_AMOUNT_WITS = 10000; const COMMIT_WEIGHT = 400; const REVEAL_WEIGHT = 200; const TALLY_WEIGHT = 100; diff --git a/lib/src/crypto/address.dart b/lib/src/crypto/address.dart index 234a3a9..08caada 100644 --- a/lib/src/crypto/address.dart +++ b/lib/src/crypto/address.dart @@ -7,6 +7,7 @@ import 'package:witnet/data_structures.dart' Utxo, UtxoPool, UtxoSelectionStrategy; +import 'package:witnet/src/utils/dotenv.dart'; import 'secp256k1/private_key.dart' show WitPrivateKey; import 'secp256k1/public_key.dart' show WitPublicKey; @@ -46,7 +47,7 @@ class Address { factory Address.fromPublicKeyHash({required Uint8List hash}) { return Address( address: bech32.encode(Bech32( - hrp: 'wit', + hrp: DotEnvUtil().testnet ? 'twit' : 'wit', data: Uint8List.fromList( convertBits(data: hash.toList(), from: 8, to: 5, pad: false)))), publicKeyHash: PublicKeyHash(hash: hash), diff --git a/lib/src/crypto/hd_wallet/extended_private_key.dart b/lib/src/crypto/hd_wallet/extended_private_key.dart index c875eb4..3857df3 100644 --- a/lib/src/crypto/hd_wallet/extended_private_key.dart +++ b/lib/src/crypto/hd_wallet/extended_private_key.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:witnet/schema.dart'; import 'package:witnet/src/crypto/hd_wallet/extended_key.dart'; import 'package:witnet/src/crypto/secp256k1/secp256k1.dart'; import 'package:witnet/src/utils/bech32/exceptions.dart'; @@ -206,6 +207,17 @@ class Xprv extends ExtendedKey { return bech1; } + PublicKeyHash get publicKeyHash => + PublicKeyHash.fromAddress(privateKey.publicKey.address); + + KeyedSignature signHash(String hash) { + final sig = privateKey.signature(hash); + return KeyedSignature( + publicKey: PublicKey(bytes: privateKey.publicKey.encode()), + signature: Signature(secp256k1: Secp256k1Signature(der: sig.encode())), + ); + } + @override Xprv child({required BigInt index}) { bool hardened = index >= BigInt.from(1 << 31); diff --git a/lib/src/crypto/secp256k1/public_key.dart b/lib/src/crypto/secp256k1/public_key.dart index 05c4a25..07e5ac6 100644 --- a/lib/src/crypto/secp256k1/public_key.dart +++ b/lib/src/crypto/secp256k1/public_key.dart @@ -1,11 +1,20 @@ import "dart:typed_data" show Uint8List; +import 'package:witnet/src/crypto/secp256k1/signature.dart'; +import 'package:witnet/utils.dart' show DotEnvUtil; + import 'secp256k1.dart' - show Point, hexToPoint, hexToPointFromCompress, pointToHexInCompress; + show + Point, + Secp256k1, + hexToPoint, + hexToPointFromCompress, + pointToHexInCompress; import 'private_key.dart' show WitPrivateKey; import '../crypto.dart' show sha256; -import 'package:witnet/utils.dart' show bech32, bytesToHex, hexToBytes; +import 'package:witnet/utils.dart' + show bech32, bytesToBigInt, bytesToHex, hexToBytes; class WitPublicKey { final Point point; @@ -31,6 +40,26 @@ class WitPublicKey { return privateKey.publicKey; } + factory WitPublicKey.recover(WitSignature signature, Uint8List message, + [int? recoveryId = null]) { + if (recoveryId != null) { + if (recoveryId >= 0 && recoveryId <= 3) { + return WitPublicKey(_recoverPublicKey(recoveryId, signature, message)); + } else { + throw ArgumentError("invalid Recovery ID: 0-3... $recoveryId"); + } + } else { + for (int recId = 0; recId <= 3; recId++) { + Point recoveredKey = _recoverPublicKey(recId, signature, message); + WitPublicKey publicKey = WitPublicKey(recoveredKey); + if (signature.verify(publicKey, bytesToHex(message))) { + return publicKey; + } + } + throw ArgumentError('Could not calculate recovery ID'); + } + } + Uint8List encode({bool compressed = true}) { return hexToBytes(pointToHexInCompress(point)); } @@ -43,7 +72,53 @@ class WitPublicKey { return sha256(data: encode()).sublist(0, 20); } - String get address { - return bech32.encodeAddress('wit', publicKeyHash); + String get address => bech32.encodeAddress( + DotEnvUtil().testnet ? 'twit' : 'wit', publicKeyHash); +} + +Point _recoverPublicKey( + int recoveryId, WitSignature signature, Uint8List message) { + BigInt z = bytesToBigInt(message); + if (signature.R >= Secp256k1.n || signature.S >= Secp256k1.n) { + throw ArgumentError("Invalid Signature"); + } + + // calculate x coordinate of point R + BigInt x = signature.R + BigInt.from(recoveryId / 2) * Secp256k1.n; + if (x >= Secp256k1.p) { + throw ArgumentError("invalid x-coordinate"); + } + + // decompress point R from the x coordinate + Point r = _decompressKey(x, (recoveryId % 2) == 1); + + BigInt e = z % Secp256k1.n; + BigInt eInv = (Secp256k1.n - e) % Secp256k1.n; + BigInt rInv = signature.R.modInverse(Secp256k1.n); + BigInt srInv = (signature.S * rInv) % Secp256k1.n; + BigInt eInvrInv = (eInv * rInv) % Secp256k1.n; + + // Q = r^-1 (sR - eG) + Point q = (r * srInv) + (Secp256k1.G * eInvrInv); + + return q; +} + +_decompressKey(BigInt xBn, bool yBit) { + var x = xBn; + + // y^2 = x^3 + ax + b (mod p) + var alpha = + (x.modPow(BigInt.from(3), Secp256k1.p) + BigInt.from(7) % Secp256k1.p); + + // y = sqrt(y^2) (mod p) + var beta = (alpha.modPow((Secp256k1.p + BigInt.one) >> 2, Secp256k1.p)); + + // select the correct y based on the yBit + var y = beta; + if ((beta.isEven ? 0 : 1) != (yBit ? 1 : 0)) { + y = Secp256k1.p - y; } + + return Point(x, y); } diff --git a/lib/src/crypto/secp256k1/secp256k1.dart b/lib/src/crypto/secp256k1/secp256k1.dart index fbfb3b1..6d07368 100644 --- a/lib/src/crypto/secp256k1/secp256k1.dart +++ b/lib/src/crypto/secp256k1/secp256k1.dart @@ -52,6 +52,10 @@ class Point { Point operator +(Point other) { return addDiffPoint(this, other, Secp256k1.p); } + + Point operator *(BigInt other) { + return pointMultiply(this, other, Secp256k1.p, Secp256k1.a); + } } Point bigIntToPoint(BigInt n) { @@ -93,6 +97,26 @@ Point addDiffPoint(Point point1, Point point2, BigInt modNum) { return Point(x3, y3); } +/// double-and-add method for point multiplication. +Point pointMultiply(Point point, BigInt k, BigInt modNum, BigInt a) { + Point result = Point(BigInt.zero, BigInt.zero); + Point addend = point; + + while (k > BigInt.zero) { + if (k.isOdd) { + if (result.x == BigInt.zero && result.y == BigInt.zero) { + result = addend; + } else { + result = addDiffPoint(result, addend, modNum); + } + } + addend = addSamePoint(addend, modNum, a); + k = k >> 1; // k = k / 2 + } + + return result; +} + Point getPointByBigInt(BigInt n, BigInt p, BigInt a, Point pointG) { var bin = n.toRadixString(2); var nextPoint = pointG; diff --git a/lib/src/crypto/secp256k1/signature.dart b/lib/src/crypto/secp256k1/signature.dart index 90e4928..97eaa05 100644 --- a/lib/src/crypto/secp256k1/signature.dart +++ b/lib/src/crypto/secp256k1/signature.dart @@ -75,6 +75,9 @@ class WitSignature { return WitSignature(r, s); } + WitPublicKey publicKey(Uint8List message) => + WitPublicKey.recover(this, message); + Uint8List encode() { Uint8List _r = bigIntToBytes(R); Uint8List _s = bigIntToBytes(S); diff --git a/lib/src/network/explorer/explorer_api.dart b/lib/src/network/explorer/explorer_api.dart index dae01dd..9813efc 100644 --- a/lib/src/network/explorer/explorer_api.dart +++ b/lib/src/network/explorer/explorer_api.dart @@ -1369,6 +1369,139 @@ class AddressMintInfo { } } +class AddressStake { + AddressStake({ + required this.epoch, + required this.timestamp, + required this.hash, + required this.direction, + required this.validator, + required this.withdrawer, + required this.input_value, + required this.stake_value, + required this.confirmed, + }); + final int epoch; + final int timestamp; + final String hash; + final String direction; + final String validator; + final String withdrawer; + final int input_value; + final int stake_value; + final bool confirmed; + + factory AddressStake.fromJson(Map data) { + return AddressStake( + epoch: data['epoch'], + timestamp: data['timestamp'], + hash: data['hash'], + direction: data['direction'], + validator: data['validator'], + withdrawer: data['withdrawer'], + input_value: data['input_value'], + stake_value: data['stake_value'], + confirmed: data['confirmed'], + ); + } + + Map jsonMap() { + return { + 'epoch': epoch, + 'timestamp': timestamp, + 'hash': hash, + 'direction': direction, + 'validator': validator, + 'withdrawer': withdrawer, + 'input_value': input_value, + 'stake_value': stake_value, + 'confirmed': confirmed, + }; + } +} + +class AddressStakes { + AddressStakes({required this.stakes}); + List stakes; + + factory AddressStakes.fromJson(List data) { + return AddressStakes( + stakes: + List.from(data.map((e) => AddressStake.fromJson(e)))); + } + + Map jsonMap() { + return { + 'stakes': List>.from(stakes.map((e) => e.jsonMap())) + }; + } +} + +class AddressUnstake { + AddressUnstake({ + required this.epoch, + required this.timestamp, + required this.hash, + required this.direction, + required this.validator, + required this.withdrawer, + required this.unstake_value, + required this.confirmed, + }); + final int epoch; + final int timestamp; + final String hash; + final String direction; + final String validator; + final String withdrawer; + final int unstake_value; + final bool confirmed; + + factory AddressUnstake.fromJson(Map data) { + return AddressUnstake( + epoch: data['epoch'], + timestamp: data['timestamp'], + hash: data['hash'], + direction: data['direction'], + validator: data['validator'], + withdrawer: data['withdrawer'], + unstake_value: data['unstake_value'], + confirmed: data['confirmed'], + ); + } + + Map jsonMap() { + return { + 'epoch': epoch, + 'timestamp': timestamp, + 'hash': hash, + 'direction': direction, + 'validator': validator, + 'withdrawer': withdrawer, + 'unstake_value': unstake_value, + 'confirmed': confirmed, + }; + } +} + +class AddressUnstakes { + AddressUnstakes({required this.unstakes}); + List unstakes; + + factory AddressUnstakes.fromJson(List data) { + return AddressUnstakes( + unstakes: List.from( + data.map((e) => AddressUnstake.fromJson(e)))); + } + + Map jsonMap() { + return { + 'unstakes': + List>.from(unstakes.map((e) => e.jsonMap())) + }; + } +} + class MintInfo { MintInfo({ required this.miner, @@ -1532,9 +1665,9 @@ class TransactionUtxo { // TODO: use this enum in all the package enum TxStatusLabel { pending, mined, confirmed, reverted, unknown } -enum MempoolTransactionType { value_transfers, data_requests } +enum MempoolTransactionType { value_transfers, data_requests, stakes, unstakes } -enum TransactionType { value_transfer, data_request, mint } +enum TransactionType { value_transfer, data_request, mint, stake, unstake } enum SupplyParams { blocks_minted, @@ -1570,6 +1703,10 @@ enum StatisticsParams { class TransactionStatus { TxStatusLabel status = TxStatusLabel.pending; TransactionStatus({required this.status}); + factory TransactionStatus.fromValues(status, reverted, confirmed) { + return TransactionStatus.fromJson( + {'status': status, 'reverted': reverted, 'confirmed': confirmed}); + } factory TransactionStatus.fromJson(Map json) { TxStatusLabel status; @@ -1678,6 +1815,175 @@ NullableFields getOrDefault(Map data) { ); } +class StakeInput { + final String address; + final int value; + + StakeInput({required this.address, required this.value}); + factory StakeInput.fromJson(Map data) { + return StakeInput(address: data['address'], value: data['value']); + } + Map jsonMap() => {'address': address, 'value': value}; +} + +class UnstakeInfo extends HashInfo { + UnstakeInfo({ + required this.hash, + required this.epoch, + required this.timestamp, + required this.block, + required this.confirmed, + required this.reverted, + required this.validator, + required this.withdrawer, + required this.unstakeValue, + required this.fee, + required this.nonce, + required this.weight, + }) : super( + txnHash: hash, + txnTime: timestamp, + status: + TransactionStatus.fromValues(null, reverted, confirmed).status, + type: TransactionType.stake, + blockHash: block); + + final String hash; + final int? epoch; + final int timestamp; + final String? block; + final bool confirmed; + final bool reverted; + final String validator; + final String withdrawer; + final int unstakeValue; + final int fee; + final int nonce; + final int weight; + + factory UnstakeInfo.fromJson(Map data) { + return UnstakeInfo( + hash: data['hash'], + epoch: data['epoch'], + timestamp: data['timestamp'], + block: data['block'], + confirmed: data['confirmed'], + reverted: data['reverted'], + validator: data['validator'], + withdrawer: data['withdrawer'], + unstakeValue: data['unstake_value'], + fee: data['fee'], + nonce: data['nonce'], + weight: data['weight'], + ); + } + + Map jsonMap() { + return { + 'hash': hash, + 'epoch': epoch, + 'timestamp': timestamp, + 'block': block, + 'confirmed': confirmed, + 'reverted': reverted, + 'validator': validator, + 'withdrawer': withdrawer, + 'unstake_value': unstakeValue, + 'fee': fee, + 'nonce': nonce, + 'weight': weight, + 'txnHash': txnHash, + 'status': status, + 'type': type, + 'txnTime': txnTime, + 'blockHash': blockHash, + }; + } +} + +class StakeInfo extends HashInfo { + StakeInfo({ + required this.block, + required this.changeAddress, + required this.changeValue, + required this.confirmed, + required this.epoch, + required this.fee, + required this.hash, + required this.inputs, + required this.priority, + required this.reverted, + required this.stakeValue, + required this.timestamp, + required this.validator, + required this.weight, + required this.withdrawer, + }) : super( + txnHash: hash, + txnTime: timestamp, + status: + TransactionStatus.fromValues(null, reverted, confirmed).status, + type: TransactionType.stake, + blockHash: block); + + final String block; + final String? changeAddress; + final int? changeValue; + final bool confirmed; + final int epoch; + final int fee; + final String hash; + final List inputs; + final int priority; + final bool reverted; + final int stakeValue; + final int timestamp; + final String validator; + final int weight; + final String withdrawer; + + factory StakeInfo.fromJson(Map data) { + return StakeInfo( + block: data['block'], + changeAddress: data['change_address'], + changeValue: data['change_value'], + confirmed: data['confirmed'], + epoch: data['epoch'], + fee: data['fee'], + hash: data['hash'], + inputs: List.from( + data["inputs"].map((x) => StakeInput.fromJson(x))), + priority: data['priority'], + reverted: data['reverted'], + stakeValue: data['stake_value'], + timestamp: data['timestamp'], + validator: data['validator'], + weight: data['weight'], + withdrawer: data['withdrawer'], + ); + } + + Map jsonMap() { + return { + 'block': block, + 'change_address': changeAddress, + 'change_value': changeValue, + 'confirmed': confirmed, + 'epoch': epoch, + 'fee': fee, + 'hash': hash, + 'inputs': List>.from(inputs.map((e) => e.jsonMap())), + 'priority': priority, + 'reverted': reverted, + 'stake_value': stakeValue, + 'timestamp': timestamp, + 'validator': validator, + 'weight': weight, + 'withdrawer': withdrawer, + }; + } +} + class ValueTransferInfo extends HashInfo { ValueTransferInfo( {required this.epoch, diff --git a/lib/src/network/explorer/explorer_client.dart b/lib/src/network/explorer/explorer_client.dart index eaafeb7..34e11ce 100644 --- a/lib/src/network/explorer/explorer_client.dart +++ b/lib/src/network/explorer/explorer_client.dart @@ -11,16 +11,18 @@ import 'package:witnet/schema.dart'; import 'explorer_api.dart' show AddressBlocks, - AddressDataRequestsSolved, AddressDataRequestsCreated, + AddressDataRequestsSolved, + AddressStakes, + AddressUnstakes, AddressValueTransfers, BlockDetails, - Mempool, Blockchain, ExplorerException, - NetworkBalances, Home, + Mempool, MintInfo, + NetworkBalances, NetworkReputation, PrioritiesEstimate, Status, @@ -65,7 +67,11 @@ class RetryHttpClient { } on http.ClientException catch (e) { if (e.message.contains('Client is already closed')) { retryClient = RetryClient(http.Client()); - response = await requestMethod(uri, body: data); + throw HttpException(e.toString()); + } + if (e.toString().contains('SocketException')) { + retryClient = RetryClient(http.Client()); + throw HttpException(e.toString()); } } if (response != null) { @@ -230,6 +236,10 @@ class ExplorerClient { case 'tally': case 'data_request_report': case 'data_request_history': + case 'stake': + return StakeInfo.fromJson(data['stake']); + case 'unstake': + return UnstakeInfo.fromJson(data['unstake']); case 'mint': return MintInfo.fromJson(data); } @@ -288,12 +298,43 @@ class ExplorerClient { } } - Future version() async { + Future explorerVersion() async { try { return await client.get(api('status'), getVersion: true); } on ExplorerException catch (e) { throw ExplorerException( - code: e.code, message: '{"version": "${e.message}"}'); + code: e.code, message: '{"explorerVersion": "${e.message}"}'); + } + } + + Future> protocolVersion([bool all = false]) async { + try { + return await client + .get(api('network/version', {'key': all ? 'all' : 'current'})); + } on ExplorerException catch (e) { + throw ExplorerException( + code: e.code, message: '{"protocolVersion": "${e.message}"}'); + } + } + + Future> stakes( + {String? validator, String? withdrawer}) async { + try { + if (validator != null || withdrawer != null) { + Map params = {}; + + if (validator != null && validator.isNotEmpty) { + params['validator'] = validator; + } + if (withdrawer != null && withdrawer.isNotEmpty) { + params['withdrawer'] = withdrawer; + } + return await client.get(api('network/stakes', params)); + } + return await client.get(api('network/stakes')); + } on ExplorerException catch (e) { + throw ExplorerException( + code: e.code, message: '{"stakes": "${e.message}"}'); } } @@ -554,6 +595,40 @@ class ExplorerClient { total: result.total, totalPages: result.totalPages, ); + case 'stakes': + PaginatedRequest result = await client.get(api( + 'address/stakes', + {'address': value, 'page': page, 'page_size': pageSize})); + List data = result.data; + if (findAll) { + data = await getAllResults(result, 'address/stakes', + {'address': value, 'page': page, 'page_size': pageSize}); + } + return PaginatedRequest( + data: AddressStakes.fromJson(data), + firstPage: result.firstPage, + lastPage: result.lastPage, + page: result.page, + total: result.total, + totalPages: result.totalPages, + ); + case 'unstakes': + PaginatedRequest result = await client.get(api( + 'address/unstakes', + {'address': value, 'page': page, 'page_size': pageSize})); + List data = result.data; + if (findAll) { + data = await getAllResults(result, 'address/unstakes', + {'address': value, 'page': page, 'page_size': pageSize}); + } + return PaginatedRequest( + data: AddressUnstakes.fromJson(data), + firstPage: result.firstPage, + lastPage: result.lastPage, + page: result.page, + total: result.total, + totalPages: result.totalPages, + ); } } on ExplorerException catch (e) { throw ExplorerException( @@ -604,6 +679,20 @@ class ExplorerClient { } } + Future nonce( + {required String validator, required String withdrawer}) async { + try { + var response = await client.get(api('transaction/nonce', + {'validator': validator, 'withdrawer': withdrawer})); + if (response.containsKey('status')) { + throw ExplorerException(code: -3, message: response['status']); + } + return response['nonce']; + } catch (e) { + rethrow; + } + } + Future valueTransferPriority() async { try { return PrioritiesEstimate.fromJson( diff --git a/lib/src/network/node/node_client.dart b/lib/src/network/node/node_client.dart index c591852..5a9e0aa 100644 --- a/lib/src/network/node/node_client.dart +++ b/lib/src/network/node/node_client.dart @@ -9,7 +9,15 @@ import 'node_api.dart' show NodeException, NodeStats, Peer, SyncStatus, UtxoInfo; import 'package:witnet/schema.dart' - show Block, ConsensusConstants, DRTransaction, Transaction, VTTransaction; + show + Block, + ConsensusConstants, + DRTransaction, + KeyedSignature, + StakeTransaction, + Transaction, + UnstakeTransaction, + VTTransaction; class NodeClient { String address; @@ -76,8 +84,8 @@ class NodeClient { }); return response; } on NodeException catch (e) { - throw NodeException( - code: e.code, message: '{"inventory": "${e.message}"}'); + print("${e.message}"); + return false; } } @@ -93,6 +101,63 @@ class NodeClient { }); } + Future sendStakeTransaction( + {required StakeTransaction stake}) async { + return await inventory({ + 'transaction': {'Stake': stake.jsonMap(asHex: false)} + }); + } + + Future sendUnstakeTransaction( + {required UnstakeTransaction unstake}) async { + try { + /// returns true if success + return await inventory({ + 'transaction': {'Unstake': unstake.jsonMap(asHex: false)} + }); + } on NodeException catch (_) { + print("${_.message}"); + return false; + } + } + + Future queryStakes(String? validator, String? withdrawer) async { + try { + Map params = {}; + if (validator != null) params['Validator'] = validator; + if (withdrawer != null) params['Withdrawer'] = withdrawer; + var _ = await sendMessage( + formatRequest(method: 'queryStakes', params: params)) + .then((Map data) { + if (data.containsKey('result')) { + return data; + } + if (data.containsKey('error')) { + return 0; + } + }); + } on NodeException catch (_) {} + return null; + } + + Future authorizeStake(String withdrawer) async { + try { + var response = await sendMessage(formatRequest( + method: 'authorizeStake', params: {'withdrawer': withdrawer})) + .then((Map data) { + if (data.containsKey('result')) { + KeyedSignature sig = + KeyedSignature.fromJson(data['result']['signature']); + return sig; + } + }); + return response!; + } on NodeException catch (e) { + throw NodeException( + code: e.code, message: '{"authorizeStake": "${e.message}"}'); + } + } + /// Get the list of all the known block hashes. Future getBlockChain( {required int epoch, required int limit}) async { diff --git a/lib/src/schema/data_request_body.dart b/lib/src/schema/data_request_body.dart index f5ca3e9..8238586 100644 --- a/lib/src/schema/data_request_body.dart +++ b/lib/src/schema/data_request_body.dart @@ -66,7 +66,8 @@ class DRTransactionBody extends GeneratedMessage { "dr_output": drOutput.jsonMap(asHex: asHex), "inputs": List.from(inputs.map((x) => x.jsonMap(asHex: asHex))), - "outputs": List.from(outputs.map((x) => x.jsonMap())), + "outputs": + List.from(outputs.map((x) => x.jsonMap(asHex: asHex))), }; @override diff --git a/lib/src/schema/keyed_signature.dart b/lib/src/schema/keyed_signature.dart index be5d413..d421690 100644 --- a/lib/src/schema/keyed_signature.dart +++ b/lib/src/schema/keyed_signature.dart @@ -46,7 +46,32 @@ class KeyedSignature extends GeneratedMessage { signature: Signature.fromJson(json["signature"]), ); - String get rawJson => json.encode(jsonMap()); + factory KeyedSignature.fromAuthorization( + {required String authorization, required String withdrawer}) { + PublicKeyHash pkh = PublicKeyHash.fromAddress(withdrawer); + Uint8List authBytes = hexToBytes(authorization); + PublicKeyHash validatorPkh = PublicKeyHash(hash: authBytes.sublist(0, 20)); + int recoveryId = authBytes[20]; + BigInt r = bytesToBigInt(authBytes.sublist(21, 53)); + BigInt s = bytesToBigInt(authBytes.sublist(53, 85)); + WitSignature signature = WitSignature(r, s); + WitPublicKey validatorKey = WitPublicKey.recover( + signature, + Uint8List(32)..setRange(0, 20, pkh.hash), + recoveryId, + ); + + assert(validatorKey.address == validatorPkh.address, + "Validator Key does not match signature"); + + return KeyedSignature( + publicKey: PublicKey(bytes: validatorKey.encode()), + signature: + Signature(secp256k1: Secp256k1Signature(der: signature.encode())), + ); + } + + String toRawJson({bool asHex = false}) => json.encode(jsonMap(asHex: asHex)); Map jsonMap({bool asHex = false}) => { "public_key": publicKey.jsonMap(asHex: asHex), diff --git a/lib/src/schema/public_key.dart b/lib/src/schema/public_key.dart index f157650..0221a8a 100644 --- a/lib/src/schema/public_key.dart +++ b/lib/src/schema/public_key.dart @@ -43,8 +43,9 @@ class PublicKey extends GeneratedMessage { @override factory PublicKey.fromJson(Map json) { Uint8List compressed = Uint8List.fromList([json['compressed']]); - Uint8List bytes = - Uint8List.fromList(List.from(json["bytes"].map((x) => x))); + Uint8List bytes = (json["bytes"].runtimeType == List) + ? Uint8List.fromList(List.from(json["bytes"].map((x) => x))) + : hexToBytes(json["bytes"]); return PublicKey(bytes: concatBytes([compressed, bytes])); } @@ -64,6 +65,10 @@ class PublicKey extends GeneratedMessage { Uint8List get pbBytes => writeToBuffer(); + PublicKeyHash get pkh => PublicKeyHash( + hash: sha256(data: Uint8List.fromList(publicKey)).sublist(0, 20), + ); + @TagNumber(1) List get publicKey => $_getN(0); @TagNumber(1) diff --git a/lib/src/schema/public_key_hash.dart b/lib/src/schema/public_key_hash.dart index 1953e49..d7f7a26 100644 --- a/lib/src/schema/public_key_hash.dart +++ b/lib/src/schema/public_key_hash.dart @@ -3,7 +3,8 @@ part of 'schema.dart'; class PublicKeyHash extends GeneratedMessage { static final BuilderInfo _i = BuilderInfo('PublicKeyHash', package: const PackageName('witnet'), createEmptyInstance: create) - ..a>(1, 'hash', PbFieldType.OY) + ..a>(1, 'hash', PbFieldType.OY, + defaultOrMaker: Uint8List.fromList(List.generate(20, (i) => 0))) ..hasRequiredFields = false; static PublicKeyHash create() => PublicKeyHash._(); @@ -41,9 +42,12 @@ class PublicKeyHash extends GeneratedMessage { String get hex => bytesToHex(Uint8List.fromList(hash)); - String get address => bech32.encodeAddress('wit', hash); + String get address => + bech32.encodeAddress(DotEnvUtil().testnet ? 'twit' : 'wit', hash); - Uint8List get pbBytes => writeToBuffer(); + Uint8List get pbBytes => hash.length == 0 + ? Uint8List.fromList(List.generate(20, (i) => 0)) + : writeToBuffer(); @TagNumber(1) List get hash => $_getN(0); diff --git a/lib/src/schema/schema.dart b/lib/src/schema/schema.dart index a904830..c0a8c37 100644 --- a/lib/src/schema/schema.dart +++ b/lib/src/schema/schema.dart @@ -1,11 +1,13 @@ import 'dart:convert' show json; import 'dart:typed_data' show Uint8List; import 'package:protobuf/protobuf.dart'; +import 'package:witnet/src/crypto/secp256k1/public_key.dart'; +import 'package:witnet/src/crypto/secp256k1/signature.dart'; import 'package:witnet/src/utils/transformations/transformations.dart'; import 'package:fixnum/fixnum.dart' show Int64; import 'package:witnet/crypto.dart' show sha256; -import 'package:witnet/utils.dart' show bech32, concatBytes; +import 'package:witnet/utils.dart' show bech32, concatBytes, DotEnvUtil; import 'package:witnet/constants.dart' show @@ -16,7 +18,9 @@ import 'package:witnet/constants.dart' REVEAL_WEIGHT, TALLY_WEIGHT, INPUT_SIZE, - OUTPUT_SIZE; + OUTPUT_SIZE, + STAKE_OUTPUT_WEIGHT, + UNSTAKE_OUTPUT_WEIGHT; import '../../radon.dart' show radToCbor, cborToRad; @@ -54,6 +58,12 @@ part 'reveal_body.dart'; part 'reveal_transaction.dart'; part 'secp256k1_signature.dart'; part 'signature.dart'; +part 'stake_body.dart'; +part 'stake_key.dart'; +part 'stake_output.dart'; +part 'stake_transaction.dart'; +part 'unstake_body.dart'; +part 'unstake_transaction.dart'; part 'string_pair.dart'; part 'super_block.dart'; part 'super_block_vote.dart'; diff --git a/lib/src/schema/stake_body.dart b/lib/src/schema/stake_body.dart new file mode 100644 index 0000000..d217aff --- /dev/null +++ b/lib/src/schema/stake_body.dart @@ -0,0 +1,132 @@ +part of 'schema.dart'; + +class StakeBody extends GeneratedMessage { + static final BuilderInfo _i = BuilderInfo('StakeBody', + package: const PackageName('witnet'), createEmptyInstance: create) + ..pc(1, 'inputs', PbFieldType.PM, subBuilder: Input.create) + ..aOM(2, 'output', subBuilder: StakeOutput.create) + ..aOM(3, 'change', + subBuilder: ValueTransferOutput.create) + ..hasRequiredFields = false; + + static StakeBody create() => StakeBody._(); + static PbList createRepeated() => PbList(); + static StakeBody getDefault() => + _defaultInstance ??= GeneratedMessage.$_defaultFor(create); + static StakeBody? _defaultInstance; + + StakeBody._() : super(); + + @override + StakeBody clone() => StakeBody()..mergeFromMessage(this); + + @override + StakeBody createEmptyInstance() => create(); + + factory StakeBody({ + Iterable? inputs, + StakeOutput? output, + ValueTransferOutput? change, + }) { + final _result = create(); + if (inputs != null) { + _result.inputs.addAll(inputs); + } + if (output != null) { + _result.output = output; + } + if (change != null) { + _result.change = change; + } + return _result; + } + + factory StakeBody.fromRawJson(String str) => + StakeBody.fromJson(json.decode(str)); + + @override + factory StakeBody.fromBuffer(List i, [r = ExtensionRegistry.EMPTY]) { + StakeBody _body = create()..mergeFromBuffer(i, r); + if (_body.change.rawJson() == ValueTransferOutput.getDefault().rawJson()) { + _body.clearChange(); + } + return _body; + } + + @override + factory StakeBody.fromJson(Map json) => StakeBody( + inputs: List.from(json["inputs"].map((x) => Input.fromJson(x))), + output: StakeOutput.fromJson(json["output"]), + change: json.containsKey('change') + ? json["change"] != null + ? ValueTransferOutput.fromJson(json["change"]) + : null + : null, + ); + + String toRawJson({bool asHex = false}) => json.encode(jsonMap(asHex: asHex)); + + Map jsonMap({bool asHex = false}) { + var _json = { + "inputs": List.from(inputs.map((x) => x.jsonMap(asHex: asHex))), + "output": output.jsonMap(asHex: asHex), + }; + if (change != ValueTransferOutput.getDefault()) { + _json['change'] = change.jsonMap(asHex: asHex); + } + return _json; + } + + Uint8List get pbBytes { + if (hasChange()) { + return writeToBuffer(); + } else { + change = ValueTransferOutput( + pkh: PublicKeyHash(hash: List.generate(20, (i) => 0))); + Uint8List _pbBytes = writeToBuffer(); + clearChange(); + return _pbBytes; + } + } + + Uint8List get hash => sha256(data: pbBytes); + + String get transactionId => bytesToHex(hash); + // VT_weight = N * INPUT_SIZE + M * OUTPUT_SIZE + STAKE_OUTPUT_WEIGHT + int get weight => + (inputs.length * INPUT_SIZE) + OUTPUT_SIZE + STAKE_OUTPUT_WEIGHT; + + @override + BuilderInfo get info_ => _i; + + @TagNumber(1) + List get inputs => $_getList(0); + + @TagNumber(2) + StakeOutput get output => $_getN(1); + @TagNumber(2) + set output(StakeOutput v) { + setField(2, v); + } + + @TagNumber(2) + bool hasOutput() => $_has(1); + @TagNumber(2) + void clearOutput() => clearField(2); + @TagNumber(2) + StakeOutput ensureOutput() => $_ensure(1); + + @TagNumber(3) + ValueTransferOutput get change => $_getN(2); + @TagNumber(3) + set change(ValueTransferOutput v) { + setField(3, v); + } + + @TagNumber(3) + bool hasChange() => $_has(2); + @TagNumber(3) + void clearChange() => clearField(3); + @TagNumber(3) + VTTransactionBody ensureChange() => $_ensure(2); +} diff --git a/lib/src/schema/stake_key.dart b/lib/src/schema/stake_key.dart new file mode 100644 index 0000000..38d9ba7 --- /dev/null +++ b/lib/src/schema/stake_key.dart @@ -0,0 +1,83 @@ +part of 'schema.dart'; + +class StakeKey extends GeneratedMessage { + static final BuilderInfo _i = BuilderInfo('StakeOutput', + package: const PackageName('witnet'), createEmptyInstance: create) + ..aOM(1, 'validator', subBuilder: PublicKeyHash.create) + ..aOM(2, 'withdrawer', subBuilder: PublicKeyHash.create) + ..hasRequiredFields = false; + + static StakeKey create() => StakeKey._(); + static PbList createRepeated() => PbList(); + static StakeKey getDefault() => + _defaultInstance ??= GeneratedMessage.$_defaultFor(create); + static StakeKey? _defaultInstance; + StakeKey._() : super(); + + @override + GeneratedMessage clone() => StakeKey()..mergeFromMessage(this); + + @override + GeneratedMessage createEmptyInstance() => create(); + + factory StakeKey({ + PublicKeyHash? validator, + PublicKeyHash? withdrawer, + }) { + final _result = create(); + if (validator != null) { + _result.validator = validator; + } + if (withdrawer != null) { + _result.withdrawer = withdrawer; + } + return _result; + } + + factory StakeKey.fromRawJson(String str) => + StakeKey.fromJson(json.decode(str)); + + @override + factory StakeKey.fromJson(Map json) => StakeKey( + validator: PublicKeyHash.fromAddress(json["validator"]), + withdrawer: PublicKeyHash.fromAddress(json["withdrawer"]), + ); + + Map jsonMap({bool asHex = false}) => { + "validator": validator.address, + "withdrawer": withdrawer.address, + }; + + @override + BuilderInfo get info_ => _i; + + Uint8List get pbBytes => writeToBuffer(); + + @TagNumber(1) + PublicKeyHash get validator => $_getN(0); + @TagNumber(1) + set validator(PublicKeyHash v) { + setField(1, v); + } + + @TagNumber(1) + bool hasValidator() => $_has(0); + @TagNumber(1) + void clearValidator() => clearField(1); + @TagNumber(1) + PublicKeyHash ensureValidator() => $_ensure(0); + + @TagNumber(2) + PublicKeyHash get withdrawer => $_getN(1); + @TagNumber(2) + set withdrawer(PublicKeyHash v) { + setField(2, v); + } + + @TagNumber(2) + bool hasWithdrawer() => $_has(1); + @TagNumber(2) + void clearWithdrawer() => clearField(2); + @TagNumber(2) + PublicKeyHash ensureWithdrawer() => $_ensure(1); +} diff --git a/lib/src/schema/stake_output.dart b/lib/src/schema/stake_output.dart new file mode 100644 index 0000000..a0af8f8 --- /dev/null +++ b/lib/src/schema/stake_output.dart @@ -0,0 +1,104 @@ +part of 'schema.dart'; + +class StakeOutput extends GeneratedMessage { + static final BuilderInfo _i = BuilderInfo('StakeOutput', + package: const PackageName('witnet'), createEmptyInstance: create) + ..a(1, 'value', PbFieldType.OU6, defaultOrMaker: Int64.ZERO) + ..aOM(2, 'key', subBuilder: StakeKey.create) + ..aOM(3, 'authorization', subBuilder: KeyedSignature.create) + ..hasRequiredFields = false; + + static StakeOutput create() => StakeOutput._(); + static PbList createRepeated() => PbList(); + static StakeOutput getDefault() => + _defaultInstance ??= GeneratedMessage.$_defaultFor(create); + static StakeOutput? _defaultInstance; + StakeOutput._() : super(); + + @override + GeneratedMessage clone() => StakeOutput()..mergeFromMessage(this); + + @override + GeneratedMessage createEmptyInstance() => create(); + + factory StakeOutput({ + int? value, + StakeKey? key, + KeyedSignature? authorization, + }) { + final _result = create(); + if (value != null) { + _result.value = Int64(value); + } + + if (key != null) { + _result.key = key; + } + + if (authorization != null) { + _result.authorization = authorization; + } + return _result; + } + + factory StakeOutput.fromRawJson(String str) => + StakeOutput.fromJson(json.decode(str)); + + @override + factory StakeOutput.fromJson(Map json) => StakeOutput( + value: json["value"], + key: StakeKey.fromJson(json["key"]), + authorization: KeyedSignature.fromJson(json["authorization"]), + ); + + Map jsonMap({bool asHex = false}) => { + "value": value.toInt(), + "key": key.jsonMap(asHex: asHex), + "authorization": authorization.jsonMap(asHex: asHex), + }; + + @override + BuilderInfo get info_ => _i; + + Uint8List get pbBytes => writeToBuffer(); + + @TagNumber(1) + Int64 get value => $_getI64(0); + @TagNumber(1) + set value(Int64 v) { + setField(1, v); + } + + @TagNumber(1) + bool hasValue() => $_has(0); + @TagNumber(1) + void clearValue() => clearField(1); + + @TagNumber(2) + StakeKey get key => $_getN(1); + @TagNumber(2) + set key(StakeKey v) { + setField(2, v); + } + + @TagNumber(2) + bool hasKey() => $_has(1); + @TagNumber(2) + void clearKey() => clearField(2); + @TagNumber(2) + StakeKey ensureKey() => $_ensure(1); + + @TagNumber(3) + KeyedSignature get authorization => $_getN(2); + @TagNumber(3) + set authorization(KeyedSignature v) { + setField(3, v); + } + + @TagNumber(3) + bool hasAuthorization() => $_has(2); + @TagNumber(3) + void clearAuthorization() => clearField(3); + @TagNumber(3) + KeyedSignature ensureAuthorization() => $_ensure(2); +} diff --git a/lib/src/schema/stake_transaction.dart b/lib/src/schema/stake_transaction.dart new file mode 100644 index 0000000..6b80ff8 --- /dev/null +++ b/lib/src/schema/stake_transaction.dart @@ -0,0 +1,89 @@ +part of 'schema.dart'; + +class StakeTransaction extends GeneratedMessage { + static final BuilderInfo _i = BuilderInfo('StakeTransaction', + package: const PackageName('witnet'), createEmptyInstance: create) + ..aOM(1, 'body', subBuilder: StakeBody.create) + ..pc(2, 'signatures', PbFieldType.PM, + subBuilder: KeyedSignature.create) + ..hasRequiredFields = false; + + static StakeTransaction create() => StakeTransaction._(); + static PbList createRepeated() => + PbList(); + static StakeTransaction getDefault() => _defaultInstance ??= + GeneratedMessage.$_defaultFor(create); + static StakeTransaction? _defaultInstance; + + StakeTransaction._() : super(); + + @override + StakeTransaction clone() => StakeTransaction()..mergeFromMessage(this); + + @override + StakeTransaction createEmptyInstance() => create(); + + factory StakeTransaction({ + StakeBody? body, + Iterable? signatures, + }) { + final _result = create(); + if (body != null) { + _result.body = body; + } + if (signatures != null) { + _result.signatures.addAll(signatures); + } + return _result; + } + + factory StakeTransaction.fromRawJson(String str) => + StakeTransaction.fromJson(json.decode(str)); + + @override + factory StakeTransaction.fromJson(Map json) => + StakeTransaction( + body: StakeBody.fromJson(json["body"]), + signatures: List.from( + json["signatures"].map((x) => KeyedSignature.fromJson(x))), + ); + + @override + factory StakeTransaction.fromBuffer(List i, + [ExtensionRegistry r = ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + + String rawJson({bool asHex = false}) => json.encode(jsonMap(asHex: asHex)); + + Map jsonMap({bool asHex = false}) => { + "body": body.jsonMap(asHex: asHex), + "signatures": + List.from(signatures.map((x) => x.jsonMap(asHex: asHex))), + }; + + String get transactionID => bytesToHex(body.hash); + + int get weight => body.weight; + + @override + BuilderInfo get info_ => _i; + + Uint8List get pbBytes => writeToBuffer(); + + @TagNumber(1) + StakeBody get body => $_getN(0); + @TagNumber(1) + set body(StakeBody v) { + setField(1, v); + } + + @TagNumber(1) + bool hasBody() => $_has(0); + @TagNumber(1) + void clearBody() => clearField(1); + @TagNumber(1) + StakeBody ensureBody() => $_ensure(0); + + @TagNumber(2) + List get signatures => $_getList(1); +} diff --git a/lib/src/schema/transaction.dart b/lib/src/schema/transaction.dart index 4a64ac9..4248b37 100644 --- a/lib/src/schema/transaction.dart +++ b/lib/src/schema/transaction.dart @@ -7,6 +7,8 @@ enum TransactionKind { reveal, tally, mint, + stake, + unstake, notSet } @@ -17,13 +19,15 @@ const Map _Transaction_KindByTag = { 4: TransactionKind.reveal, 5: TransactionKind.tally, 6: TransactionKind.mint, + 7: TransactionKind.stake, + 8: TransactionKind.unstake, 0: TransactionKind.notSet }; class Transaction extends GeneratedMessage { static final BuilderInfo _i = BuilderInfo('Transaction', package: const PackageName('witnet'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8]) ..aOM(1, 'ValueTransfer', protoName: 'ValueTransfer', subBuilder: VTTransaction.create) ..aOM(2, 'DataRequest', @@ -36,6 +40,10 @@ class Transaction extends GeneratedMessage { protoName: 'Tally', subBuilder: TallyTransaction.create) ..aOM(6, 'Mint', protoName: 'Mint', subBuilder: MintTransaction.create) + ..aOM(7, 'Stake', + protoName: 'Stake', subBuilder: StakeTransaction.create) + ..aOM(8, 'Unstake', + protoName: 'Unstake', subBuilder: UnstakeTransaction.create) ..hasRequiredFields = false; static Transaction create() => Transaction._(); @@ -59,6 +67,8 @@ class Transaction extends GeneratedMessage { RevealTransaction? reveal, TallyTransaction? tally, MintTransaction? mint, + StakeTransaction? stake, + UnstakeTransaction? unstake, }) { final _result = create(); if (valueTransfer != null) { @@ -79,6 +89,12 @@ class Transaction extends GeneratedMessage { if (mint != null) { _result.mint = mint; } + if (stake != null) { + _result.stake = stake; + } + if (unstake != null) { + _result.unstake = unstake; + } return _result; } @@ -110,9 +126,13 @@ class Transaction extends GeneratedMessage { case 'Reveal': return Transaction( reveal: RevealTransaction.fromJson(_txn['Reveal'])); - case 'Tally': return Transaction(tally: TallyTransaction.fromJson(_txn['Tally'])); + case 'Stake': + return Transaction(stake: StakeTransaction.fromJson(_txn['Stake'])); + case 'Unstake': + return Transaction( + unstake: UnstakeTransaction.fromJson(_txn['Unstake'])); } } else { throw ArgumentError('Invalid json'); @@ -123,16 +143,55 @@ class Transaction extends GeneratedMessage { String toRawJson({bool asHex = false}) => json.encode(jsonMap(asHex: asHex)); Map jsonMap({bool asHex = false}) { - final txType = hasDataRequest() ? 'DataRequest' : 'ValueTransfer'; - return { - "transaction": { - txType: { - "body": transaction.body.jsonMap(asHex: asHex), - "signatures": List.from( - transaction.signatures.map((x) => x.jsonMap(asHex: asHex))), + if (hasValueTransfer()) + return { + "transaction": { + 'ValueTransfer': { + "body": transaction.body.jsonMap(asHex: asHex), + "signatures": List.from( + transaction.signatures.map((x) => x.jsonMap(asHex: asHex))), + } } - } - }; + }; + if (hasDataRequest()) + return { + "transaction": { + 'DataRequest': { + "body": transaction.body.jsonMap(asHex: asHex), + "signatures": List.from( + transaction.signatures.map((x) => x.jsonMap(asHex: asHex))), + } + } + }; + if (hasStake()) + return { + "transaction": { + 'Stake': { + "body": transaction.body.jsonMap(asHex: asHex), + "signatures": List.from( + transaction.signatures.map((x) => x.jsonMap(asHex: asHex))), + }, + } + }; + if (hasUnstake()) + return { + "transaction": { + 'Unstake': { + "body": transaction.body.jsonMap(asHex: asHex), + "signature": transaction.signature.jsonMap(asHex: asHex), + } + } + }; + else + return { + "transaction": { + 'ValueTransfer': { + "body": transaction.body.jsonMap(asHex: asHex), + "signatures": List.from( + transaction.signatures.map((x) => x.jsonMap(asHex: asHex))), + } + } + }; } @override @@ -161,6 +220,8 @@ class Transaction extends GeneratedMessage { if (hasReveal()) return reveal; if (hasTally()) return tally; if (hasMint()) return mint; + if (hasStake()) return stake; + if (hasUnstake()) return unstake; } TransactionKind whichKind() => _Transaction_KindByTag[$_whichOneof(0)]!; @@ -250,4 +311,32 @@ class Transaction extends GeneratedMessage { void clearMint() => clearField(6); @TagNumber(6) MintTransaction ensureMint() => $_ensure(5); + + @TagNumber(7) + StakeTransaction get stake => $_getN(6); + @TagNumber(7) + set stake(StakeTransaction v) { + setField(7, v); + } + + @TagNumber(7) + bool hasStake() => $_has(6); + @TagNumber(7) + void clearStake() => clearField(7); + @TagNumber(7) + StakeTransaction ensureStake() => $_ensure(6); + + @TagNumber(8) + UnstakeTransaction get unstake => $_getN(7); + @TagNumber(8) + set unstake(UnstakeTransaction v) { + setField(8, v); + } + + @TagNumber(8) + bool hasUnstake() => $_has(7); + @TagNumber(8) + void clearUnstake() => clearField(8); + @TagNumber(8) + UnstakeTransaction ensureUnstake() => $_ensure(7); } diff --git a/lib/src/schema/unstake_body.dart b/lib/src/schema/unstake_body.dart new file mode 100644 index 0000000..98ddcaf --- /dev/null +++ b/lib/src/schema/unstake_body.dart @@ -0,0 +1,139 @@ +part of 'schema.dart'; + +class UnstakeBody extends GeneratedMessage { + static final BuilderInfo _i = BuilderInfo('UnstakeBody', + package: const PackageName('witnet'), createEmptyInstance: create) + ..aOM(1, 'operator', subBuilder: PublicKeyHash.create) + ..aOM(2, 'withdrawal', + subBuilder: ValueTransferOutput.create) + ..a(3, 'fee', PbFieldType.OU6, defaultOrMaker: Int64.ZERO) + ..a(4, 'nonce', PbFieldType.OU6, defaultOrMaker: Int64.ZERO) + ..hasRequiredFields = false; + + static UnstakeBody create() => UnstakeBody._(); + static UnstakeBody getDefault() => + _defaultInstance ??= GeneratedMessage.$_defaultFor(create); + static UnstakeBody? _defaultInstance; + + UnstakeBody._() : super(); + + @override + UnstakeBody clone() => UnstakeBody()..mergeFromMessage(this); + + @override + UnstakeBody createEmptyInstance() => create(); + + factory UnstakeBody({ + PublicKeyHash? operator, + ValueTransferOutput? withdrawal, + int? fee, + int? nonce, + }) { + final _result = create(); + if (operator != null) { + _result.operator = operator; + } + if (withdrawal != null) { + _result.withdrawal = withdrawal; + } + if (fee != null) { + _result.fee = Int64(fee); + } + if (nonce != null) { + _result.nonce = Int64(nonce); + } + return _result; + } + + factory UnstakeBody.fromRawJson(String str) => + UnstakeBody.fromJson(json.decode(str)); + + @override + factory UnstakeBody.fromBuffer(List i, + [ExtensionRegistry r = ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + + @override + factory UnstakeBody.fromJson(Map json) => UnstakeBody( + operator: PublicKeyHash.fromAddress(json["operator"]), + withdrawal: ValueTransferOutput.fromJson(json["withdrawal"]), + fee: json.containsKey('fee') ? json["fee"] : null, + nonce: json["nonce"], + ); + + factory UnstakeBody.fromPbBytes(Uint8List buffer) => + create()..mergeFromBuffer(buffer, ExtensionRegistry.EMPTY); + + String toRawJson({bool asHex = false}) => json.encode(jsonMap( + asHex: asHex, + )); + + Map jsonMap({bool asHex = false}) => { + "operator": operator.address, + "withdrawal": withdrawal.jsonMap(asHex: asHex), + "fee": fee.toInt(), + "nonce": nonce.toInt(), + }; + + Uint8List get pbBytes => writeToBuffer(); + + Uint8List get hash => sha256(data: pbBytes); + + // VT_weight = 153 + int get weight => UNSTAKE_OUTPUT_WEIGHT; + + @override + BuilderInfo get info_ => _i; + + @TagNumber(1) + PublicKeyHash get operator => $_getN(0); + @TagNumber(1) + set operator(PublicKeyHash v) { + setField(1, v); + } + + @TagNumber(1) + bool hasOperator() => $_has(0); + @TagNumber(1) + void clearOperator() => clearField(1); + @TagNumber(1) + PublicKeyHash ensureOperator() => $_ensure(0); + + @TagNumber(2) + ValueTransferOutput get withdrawal => $_getN(1); + @TagNumber(2) + set withdrawal(ValueTransferOutput v) { + setField(2, v); + } + + @TagNumber(2) + bool hasWithdrawal() => $_has(1); + @TagNumber(2) + void clearWithdrawal() => clearField(2); + @TagNumber(2) + ValueTransferOutput ensureWithdrawal() => $_ensure(1); + + @TagNumber(3) + Int64 get fee => $_getI64(2); + @TagNumber(3) + set fee(Int64 v) { + (v == Int64.ZERO) ? null : $_setInt64(2, v); + } + + @TagNumber(3) + bool hasFee() => $_has(2); + @TagNumber(3) + void clearFee() => clearField(3); + + @TagNumber(4) + Int64 get nonce => $_getI64(3); + @TagNumber(4) + set nonce(Int64 v) { + $_setInt64(3, v); + } + + @TagNumber(4) + bool hasNonce() => $_has(3); + @TagNumber(4) + void clearNonce() => clearField(4); +} diff --git a/lib/src/schema/unstake_transaction.dart b/lib/src/schema/unstake_transaction.dart new file mode 100644 index 0000000..1848dbc --- /dev/null +++ b/lib/src/schema/unstake_transaction.dart @@ -0,0 +1,97 @@ +part of 'schema.dart'; + +class UnstakeTransaction extends GeneratedMessage { + static final BuilderInfo _i = BuilderInfo('UnstakeTransaction', + package: const PackageName('witnet'), createEmptyInstance: create) + ..aOM(1, 'body', subBuilder: UnstakeBody.create) + ..aOM(2, 'signature', subBuilder: KeyedSignature.create) + ..hasRequiredFields = false; + + static UnstakeTransaction create() => UnstakeTransaction._(); + static PbList createRepeated() => + PbList(); + static UnstakeTransaction getDefault() => _defaultInstance ??= + GeneratedMessage.$_defaultFor(create); + static UnstakeTransaction? _defaultInstance; + + UnstakeTransaction._() : super(); + + @override + UnstakeTransaction clone() => UnstakeTransaction()..mergeFromMessage(this); + + @override + UnstakeTransaction createEmptyInstance() => create(); + + factory UnstakeTransaction({ + UnstakeBody? body, + KeyedSignature? signature, + }) { + final _result = create(); + if (body != null) { + _result.body = body; + } + if (signature != null) { + _result.signature = signature; + } + return _result; + } + + factory UnstakeTransaction.fromRawJson(String str) => + UnstakeTransaction.fromJson(json.decode(str)); + + @override + factory UnstakeTransaction.fromJson(Map json) => + UnstakeTransaction( + body: UnstakeBody.fromJson(json["body"]), + signature: KeyedSignature.fromJson(json["signature"]), + ); + + @override + factory UnstakeTransaction.fromBuffer(List i, + [ExtensionRegistry r = ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + + String rawJson({bool asHex = false}) => json.encode(jsonMap(asHex: asHex)); + + Map jsonMap({bool asHex = false}) => { + "body": body.jsonMap(asHex: asHex), + "signature": signature.jsonMap(asHex: asHex) + }; + + String get transactionID => bytesToHex(body.hash); + + int get weight => body.weight; + + @override + BuilderInfo get info_ => _i; + + Uint8List get pbBytes => writeToBuffer(); + + @TagNumber(1) + UnstakeBody get body => $_getN(0); + @TagNumber(1) + set body(UnstakeBody v) { + setField(1, v); + } + + @TagNumber(1) + bool hasBody() => $_has(0); + @TagNumber(1) + void clearBody() => clearField(1); + @TagNumber(1) + UnstakeBody ensureBody() => $_ensure(0); + + @TagNumber(2) + KeyedSignature get signature => $_getN(1); + + @TagNumber(2) + bool hasSignature() => $_has(1); + @TagNumber(2) + void clearSignature() => clearField(2); + @TagNumber(2) + UnstakeBody ensureSignature() => $_ensure(1); + @TagNumber(2) + set signature(KeyedSignature v) { + setField(2, v); + } +} diff --git a/lib/src/schema/value_transfer_body.dart b/lib/src/schema/value_transfer_body.dart index 09cc989..8ddf135 100644 --- a/lib/src/schema/value_transfer_body.dart +++ b/lib/src/schema/value_transfer_body.dart @@ -59,8 +59,9 @@ class VTTransactionBody extends GeneratedMessage { String toRawJson({bool asHex = false}) => json.encode(jsonMap(asHex: asHex)); Map jsonMap({bool asHex = false}) => { - "inputs": - List.from(inputs.map((x) => x.jsonMap(asHex: asHex))), + "inputs": List.from(inputs.map((x) => x.jsonMap( + asHex: asHex, + ))), "outputs": List.from(outputs.map((x) => x.jsonMap(asHex: asHex))), }; diff --git a/lib/src/utils/dotenv.dart b/lib/src/utils/dotenv.dart new file mode 100644 index 0000000..6443750 --- /dev/null +++ b/lib/src/utils/dotenv.dart @@ -0,0 +1,25 @@ +import 'package:dotenv/dotenv.dart'; + +class DotEnvUtil { + DotEnv? env; + static final DotEnvUtil _singleton = DotEnvUtil._internal(); + factory DotEnvUtil() { + if (_singleton.env == null) { + _singleton.env = DotEnv(includePlatformEnvironment: true)..load(); + } + return _singleton; + } + + DotEnvUtil._internal(); + + bool get testnet => get('ENVIRONMENT') != 'testnet' ? false : true; + + String? get(String key) { + if (env != null) { + if (env!.isDefined(key)) { + return env![key]; + } + } + return null; + } +} diff --git a/lib/utils.dart b/lib/utils.dart index f691f08..ee34b6a 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -23,3 +23,4 @@ export 'src/utils/bech32/codec.dart' show bech32; export 'src/utils/bech32/decoder.dart' show Bech32Decoder; export 'src/utils/bech32/bech32.dart' show Bech32; export 'src/utils/bech32/validations.dart' show Bech32Validations; +export 'src/utils/dotenv.dart' show DotEnvUtil; diff --git a/pubspec.yaml b/pubspec.yaml index ee8eeaf..34c3e4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: A library to interface with the Witnet Protocol. - communicate with a rust node or wallet server - communicate with the explorer -version: 0.4.3 +version: 0.4.4 homepage: https://github.com/witnet/witnet.dart repository: https://github.com/witnet/witnet.dart @@ -30,4 +30,5 @@ dependencies: protobuf: ^3.1.0 fixnum: ^1.1.0 recase: ^4.1.0 + dotenv: ^4.2.0