Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"addressBalanceDescription": "Received payments totalling",
"addressCopied": "Address copied!",
"addressList": "Address list",
"addTimelockLabel": "Add Timelock (Optional)",
"addAdvancedSettings": "Add Advanced Settings (Optional)",
"advancedSettings": "Advanced Settings",
"amount": "Amount",
"authenticateWithBiometrics": "Authenticate with biometrics",
Expand Down Expand Up @@ -232,6 +232,7 @@
"messageSigning": "Message Signing",
"messageSigning01": "Prove the ownership of your address by adding your signature to a message.",
"messageToBeSigned": "Message to be signed",
"metadata": "Metadata",
"mined": "Mined",
"minerFeeHint": "By default, 'Absolute fee' is selected.\nTo set a custom weighted fee, you need to select 'Weighted'. \nThe Weighted fee is automatically calculated by the wallet considering the network congestion and transaction weight multiplied by the value selected as custom.",
"minerFeeInputHint": "Input the miner fee",
Expand Down
3 changes: 2 additions & 1 deletion lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"addressBalanceDescription": "Recibió pagos por un total de",
"addressCopied": "¡Dirección copiada!",
"addressList": "Lista de direcciones",
"addTimelockLabel": "Añadir Timelock (Opcional)",
"addAdvancedSettings": "Añadir Opciones avanzadas (Opcional)",
"advancedSettings": "Configuración avanzada",
"amount": "Cantidad",
"authenticateWithBiometrics": "Autenticación biométrica",
Expand Down Expand Up @@ -230,6 +230,7 @@
"messageSigning": "Firma de mensajes",
"messageSigning01": "Demuestre la propiedad de su dirección añadiendo su firma a un mensaje.",
"messageToBeSigned": "Mensaje a firmar",
"metadata": "Metadatos",
"mined": "Minada",
"minerFeeHint": "Por defecto, la tarifa Absoluta está seleccionada. La comisión ponderada la calcula automáticamente el monedero teniendo en cuenta la congestión de la red y el peso de la transacción multiplicado por el valor seleccionado como personalizado.",
"minerFeeInputHint": "Introduzca la tasa minera",
Expand Down
10 changes: 10 additions & 0 deletions lib/util/extensions/string_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ extension StringExtension on String {
.join(' ');

bool toBoolean() => this == 'true' || this == 'True';

bool isHexString() {
final hexRegex = RegExp(r'^[a-fA-F0-9]+$');

if (this.startsWith("0x")) {
return hexRegex.hasMatch(this.substring(2));
} else {
return hexRegex.hasMatch(this);
}
}
}
75 changes: 75 additions & 0 deletions lib/util/metadata_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'dart:convert';

import 'package:witnet/schema.dart';

String? metadataFromOutputs(
List<ValueTransferOutput> outputs, {
List<String> trueOutputAddresses = const [],
List<String> changeOutputAddresses = const [],
}) {
if (outputs.isEmpty) {
return null;
}

Iterable<ValueTransferOutput> candidates =
outputs.where((output) => output.value.toInt() == 1);

if (trueOutputAddresses.isNotEmpty || changeOutputAddresses.isNotEmpty) {
final excluded = <String>{
...trueOutputAddresses,
...changeOutputAddresses,
};
final filtered = candidates
.where((output) => !excluded.contains(output.pkh.address))
.toList();
if (filtered.isNotEmpty) {
candidates = filtered;
}
}

if (candidates.isEmpty) {
return null;
}

return _decodeMetadataOutput(candidates.first);
}

String? _decodeMetadataOutput(ValueTransferOutput output) {
final bytes = List<int>.from(output.pkh.hash);
int end = bytes.length;
while (end > 0 && bytes[end - 1] == 0) {
end--;
}
if (end == 0) {
return null;
}

final trimmed = bytes.sublist(0, end);
String decoded;
try {
decoded = utf8.decode(trimmed, allowMalformed: true);
} catch (_) {
return '0x${output.pkh.hex}';
}

if (_isPrintable(decoded)) {
return decoded;
}

return '0x${output.pkh.hex}';
}

bool _isPrintable(String value) {
if (value.contains('\uFFFD')) {
return false;
}
for (final rune in value.runes) {
if (rune == 0x09 || rune == 0x0A || rune == 0x0D) {
continue;
}
if (rune < 0x20 || rune == 0x7F) {
return false;
}
}
return true;
}
9 changes: 9 additions & 0 deletions lib/util/transactions_list/transaction_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:my_wit_wallet/shared/locator.dart';
import 'package:my_wit_wallet/theme/colors.dart';
import 'package:my_wit_wallet/theme/extended_theme.dart';
import 'package:my_wit_wallet/util/get_localization.dart';
import 'package:my_wit_wallet/util/metadata_utils.dart';
import 'package:my_wit_wallet/util/storage/database/account.dart';
import 'package:my_wit_wallet/util/storage/database/adapters/transaction_adapter.dart';
import 'package:my_wit_wallet/util/storage/database/wallet.dart';
Expand Down Expand Up @@ -308,6 +309,14 @@ class TransactionUtils {
}
}

String? metadata() {
if (vti.type != TransactionType.value_transfer || vti.vtt == null) {
return null;
}

return metadataFromOutputs(vti.vtt!.outputs);
}

Widget buildTransactionValue(label, context) {
final theme = Theme.of(context);
final extendedTheme = theme.extension<ExtendedTheme>()!;
Expand Down
115 changes: 115 additions & 0 deletions lib/widgets/inputs/input_metadata.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:my_wit_wallet/widgets/inputs/input_text.dart';
import 'package:my_wit_wallet/widgets/suffix_icon_button.dart';
import 'package:my_wit_wallet/widgets/witnet/transactions/value_transfer/create_dialog_box/qr_scanner.dart';

import 'package:my_wit_wallet/theme/extended_theme.dart';
import 'package:my_wit_wallet/util/get_localization.dart';
import 'package:my_wit_wallet/util/storage/scanned_content.dart';

class InputMetadata extends InputText {
InputMetadata({
required super.focusNode,
required super.styledTextController,
super.prefixIcon,
super.errorText,
super.validator,
super.hint,
super.keyboardType,
super.obscureText = false,
this.route,
super.onChanged,
super.onEditingComplete,
super.onFieldSubmitted,
super.onTapOutside,
super.onTap,
super.onSuffixTap,
super.inputFormatters,
super.decoration,
super.maxLines = 1,
this.setMetadataCallback,
});

final String? route;
final void Function(String, {bool? validate})? setMetadataCallback;
@override
_InputMetadataState createState() => _InputMetadataState();
}

class _InputMetadataState extends State<InputMetadata> {
FocusNode _scanQrFocusNode = FocusNode();
bool isScanQrFocused = false;
ScannedContent scannedContent = ScannedContent();
TextSelection? lastSelection;

@override
void initState() {
super.initState();
if (scannedContent.scannedContent != null) {
_handleQrMetadataResults(scannedContent.scannedContent!);
}
widget.focusNode.addListener(widget.onFocusChange);
_scanQrFocusNode.addListener(_handleQrFocus);
}

@override
void dispose() {
super.dispose();
widget.focusNode.removeListener(widget.onFocusChange);
_scanQrFocusNode.removeListener(_handleQrFocus);
}

_handleQrFocus() {
setState(() {
isScanQrFocused = _scanQrFocusNode.hasFocus;
});
}

_handleQrMetadataResults(String value) {
widget.styledTextController.text = value;
widget.setMetadataCallback!(value);
}

Widget build(BuildContext context) {
final theme = Theme.of(context);
final extendedTheme = theme.extension<ExtendedTheme>()!;

widget.styledTextController.setStyle(
extendedTheme.monoLargeText!
.copyWith(color: theme.textTheme.bodyMedium!.color),
extendedTheme.monoLargeText!.copyWith(color: Colors.black),
);

return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: [
widget.buildInput(
context: context,
decoration: widget.decoration ??
InputDecoration(
hintStyle: extendedTheme.monoLargeText!.copyWith(
color: theme.inputDecorationTheme.hintStyle!.color),
hintText: localization.metadata,
suffixIcon: !Platform.isWindows && !Platform.isLinux
? Semantics(
label: localization.scanQrCodeLabel,
child: SuffixIcon(
onPressed: () => {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => QrScanner(
currentRoute: widget.route!,
onChanged: (_value) => {})))
},
icon: FontAwesomeIcons.qrcode,
isFocus: isScanQrFocused,
focusNode: _scanQrFocusNode))
: null,
errorText: widget.errorText,
)),
]);
}
}
10 changes: 9 additions & 1 deletion lib/widgets/transaction_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ class TransactionDetails extends StatelessWidget {
TransactionUtils transactionUtils = TransactionUtils(vti: transaction);
String label = transactionUtils.getLabel();
String? timelock = transactionUtils.timelock();
String? metadata = transactionUtils.metadata();

return ClosableView(
closeSetting: goToList,
Expand Down Expand Up @@ -302,7 +303,14 @@ class TransactionDetails extends StatelessWidget {
label: localization.total,
text: transactionUtils.getTransactionValue().amount,
isContentImportant: true,
isLastItem: timelock == null),
isLastItem: timelock == null && metadata == null),
if (metadata != null)
InfoCopy(
label: localization.metadata,
text: metadata.cropMiddle(18),
infoToCopy: metadata,
isContentImportant: true,
isLastItem: timelock == null),
timelock != null
? InfoElement(
label: localization.timelock,
Expand Down
46 changes: 46 additions & 0 deletions lib/widgets/validations/metadata_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:formz/formz.dart';
import 'package:my_wit_wallet/util/extensions/string_extensions.dart';
import 'package:my_wit_wallet/widgets/validations/validation_utils.dart';

// Define input validation errors
enum MetadataInputError { invalidHex, invalidLength }

Map<MetadataInputError, String> errorMap = {
MetadataInputError.invalidHex: 'Invalid hex metadata',
MetadataInputError.invalidLength: 'Invalid length metadata',
};

// Extend FormzInput and provide the input type and error type.
class MetadataInput extends FormzInput<String, String?> {
final bool allowValidation;
// Call super.pure to represent an unmodified form input.
const MetadataInput.pure()
: allowValidation = false,
super.pure('');

// Call super.dirty to represent a modified form input.
const MetadataInput.dirty({String value = '', this.allowValidation = false})
: super.dirty(value);

// Override validator to handle validating a given input value.
@override
String? validator(String? value) {
final _validationUtils = ValidationUtils(errorMap: errorMap);

if (!this.allowValidation) {
return null;
}

if (value != null) {
if (!value.isHexString()) {
return _validationUtils.getErrorText(MetadataInputError.invalidHex);
}

if (!((value.startsWith("0x") && (value.substring(2).length == 20 * 2)) ||
(value.length == 20 * 2))) {
return _validationUtils.getErrorText(MetadataInputError.invalidLength);
}
}
return null;
}
}
Loading
Loading