diff --git a/lib/data_structures.dart b/lib/data_structures.dart index d266d45..88558a1 100644 --- a/lib/data_structures.dart +++ b/lib/data_structures.dart @@ -1,5 +1,10 @@ export 'src/data_structures/utxo_pool.dart' show UtxoPool, UtxoSelectionStrategy; export 'src/data_structures/transaction_factory.dart' - show FeeType, TransactionInfo, createDRTransaction, createVTTransaction; + show + FeeType, + TransactionInfo, + createDRTransaction, + createVTTransaction, + createMetadataOutput; export 'src/data_structures/utxo.dart'; diff --git a/lib/src/crypto/address.dart b/lib/src/crypto/address.dart index 08caada..9639f74 100644 --- a/lib/src/crypto/address.dart +++ b/lib/src/crypto/address.dart @@ -91,6 +91,7 @@ class Address { FeeType? feeType, int fee = 0, required dynamic networkSource, + String? metadata, }) async { return await createVTTransaction( outputs: outputs, @@ -101,7 +102,8 @@ class Address { feeType: feeType, fee: fee, utxoStrategy: utxoStrategy, - networkSource: networkSource); + networkSource: networkSource, + metadata: metadata); } Future createDRT({ diff --git a/lib/src/data_structures/transaction_factory.dart b/lib/src/data_structures/transaction_factory.dart index 706a221..560b9dd 100644 --- a/lib/src/data_structures/transaction_factory.dart +++ b/lib/src/data_structures/transaction_factory.dart @@ -12,7 +12,11 @@ import 'package:witnet/src/crypto/address.dart' show Address; import 'package:witnet/src/crypto/secp256k1/private_key.dart'; import 'package:witnet/constants.dart' show ALPHA, GAMMA, INPUT_SIZE, OUTPUT_SIZE; +import 'package:witnet/src/schema/schema.dart' show PublicKeyHash; +import 'package:witnet/src/utils/transformations/transformations.dart' + show isHexStringOfLength; import '../../data_structures.dart'; +import 'package:convert/convert.dart'; class TransactionInfo { final List inputs; @@ -34,6 +38,26 @@ enum FeeType { Weighted, } +ValueTransferOutput createMetadataOutput(String metadata) { + if (!isHexStringOfLength(metadata, 20)) { + throw TransactionError(-1, 'Metadata must be a 20-byte hex string'); + } + + final cleanMetadata = + metadata.startsWith('0x') ? metadata.substring(2) : metadata; + + // Convert hex string into raw List of bytes + final metadataBytes = hex.decode(cleanMetadata); + + final metadataPkh = PublicKeyHash()..hash = metadataBytes; + + return ValueTransferOutput.fromJson({ + 'pkh': metadataPkh.address, + 'value': 1, + 'time_lock': 0, + }); +} + Future createVTTransaction({ required List outputs, required WitPrivateKey privateKey, @@ -42,11 +66,18 @@ Future createVTTransaction({ required UtxoSelectionStrategy utxoStrategy, FeeType? feeType, int fee = 0, + String? metadata, }) async { + List allOutputs = outputs; + + if (metadata != null) { + allOutputs.add(createMetadataOutput(metadata)); + } + int outputValue = 0; int totalUtxoValue = 0; int selectedUtxoValue = 0; - outputs.forEach((ValueTransferOutput output) { + allOutputs.forEach((ValueTransferOutput output) { outputValue += output.value.toInt(); }); @@ -92,7 +123,7 @@ Future createVTTransaction({ switch (feeType ?? absFee) { case FeeType.Absolute: if (change > fee) { - outputs.add(_changeAddress.receive(change - fee)); + allOutputs.add(_changeAddress.receive(change - fee)); } else if (change == fee) { // do nothing with the change since it is the fee. } else { @@ -110,7 +141,7 @@ Future createVTTransaction({ case FeeType.Weighted: print('Current Change: $change'); var inputCount = inputs.length; - var outputCount = outputs.length; + var outputCount = allOutputs.length; var currentWeight = vttWeight(inputCount, outputCount); print('Inputs: $inputCount, Outputs: $outputCount'); print('Current Weight -> $currentWeight'); @@ -131,7 +162,7 @@ Future createVTTransaction({ newWeight = vttWeight(inputs.length, outputCount + 1); } } - outputs.add(_changeAddress.receive(change - newWeight)); + allOutputs.add(_changeAddress.receive(change - newWeight)); } else { // need additional utxos to cover the weighted fee } @@ -139,7 +170,8 @@ Future createVTTransaction({ } } - VTTransactionBody body = VTTransactionBody(inputs: inputs, outputs: outputs); + VTTransactionBody body = + VTTransactionBody(inputs: inputs, outputs: allOutputs); VTTransaction transaction = VTTransaction(body: body, signatures: []); KeyedSignature signature = diff --git a/lib/src/schema/extensions/value_transfer_transaction.dart b/lib/src/schema/extensions/value_transfer_transaction.dart new file mode 100644 index 0000000..2fb0ed5 --- /dev/null +++ b/lib/src/schema/extensions/value_transfer_transaction.dart @@ -0,0 +1,14 @@ +import 'package:witnet/schema.dart'; + +extension Metadata on VTTransaction { + List get metadata { + List metadata = this + .body + .outputs + .where((ValueTransferOutput output) => output.value == 1) + .map((ValueTransferOutput output) => output.pkh) + .toList(); + + return metadata; + } +} diff --git a/lib/src/utils/transformations/transformations.dart b/lib/src/utils/transformations/transformations.dart index 2f70e2a..bde3433 100644 --- a/lib/src/utils/transformations/transformations.dart +++ b/lib/src/utils/transformations/transformations.dart @@ -184,3 +184,19 @@ Uint8List toUtf16Bytes(String string, } bool isStringNullOrEmpty(String? string) => string == null || string.isEmpty; + +bool isHexStringOfLength(String str, int length) { + return isHexString(str) && + ((str.startsWith("0x") && (str.substring(2).length == length * 2)) || + (str.length == length * 2)); +} + +bool isHexString(String str) { + final hexRegex = RegExp(r'^[a-fA-F0-9]+$'); + + if (str.startsWith("0x")) { + return hexRegex.hasMatch(str.substring(2)); + } else { + return hexRegex.hasMatch(str); + } +} diff --git a/test/data_structures/create_metadata_output_test.dart b/test/data_structures/create_metadata_output_test.dart new file mode 100644 index 0000000..0e2eea2 --- /dev/null +++ b/test/data_structures/create_metadata_output_test.dart @@ -0,0 +1,20 @@ +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; +import 'package:witnet/schema.dart'; +import 'package:witnet/src/data_structures/transaction_factory.dart'; + +void main() async { + group("create_metadata_output", () { + test("Create Metadata Output", () { + ValueTransferOutput vto1 = + createMetadataOutput("0x1111AbA2164AcdC6D291b08DfB374280035E1111"); + expect(vto1.value.toInt(), 1); + expect(vto1.pkh.address, 'wit1zyg6hgskftxud553kzxlkd6zsqp4uyg3zd9q6h'); + + ValueTransferOutput vto2 = + createMetadataOutput("0x77703aE126B971c9946d562F41Dd47071dA00777"); + expect(vto2.value.toInt(), 1); + expect(vto2.pkh.address, 'wit1wacr4cfxh9cun9rd2ch5rh28quw6qpmhk0ydga'); + }); + }); +}