Skip to content

Commit 6ffd8f0

Browse files
committed
feat: implement metadata extraction and display in transaction flow
1 parent 7c5e9fb commit 6ffd8f0

File tree

5 files changed

+142
-6
lines changed

5 files changed

+142
-6
lines changed

lib/util/metadata_utils.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import 'dart:convert';
2+
3+
import 'package:witnet/schema.dart';
4+
5+
String? metadataFromOutputs(
6+
List<ValueTransferOutput> outputs, {
7+
List<String> trueOutputAddresses = const [],
8+
List<String> changeOutputAddresses = const [],
9+
}) {
10+
if (outputs.isEmpty) {
11+
return null;
12+
}
13+
14+
Iterable<ValueTransferOutput> candidates =
15+
outputs.where((output) => output.value.toInt() == 1);
16+
17+
if (trueOutputAddresses.isNotEmpty || changeOutputAddresses.isNotEmpty) {
18+
final excluded = <String>{
19+
...trueOutputAddresses,
20+
...changeOutputAddresses,
21+
};
22+
final filtered = candidates
23+
.where((output) => !excluded.contains(output.pkh.address))
24+
.toList();
25+
if (filtered.isNotEmpty) {
26+
candidates = filtered;
27+
}
28+
}
29+
30+
if (candidates.isEmpty) {
31+
return null;
32+
}
33+
34+
return _decodeMetadataOutput(candidates.first);
35+
}
36+
37+
String? _decodeMetadataOutput(ValueTransferOutput output) {
38+
final bytes = List<int>.from(output.pkh.hash);
39+
int end = bytes.length;
40+
while (end > 0 && bytes[end - 1] == 0) {
41+
end--;
42+
}
43+
if (end == 0) {
44+
return null;
45+
}
46+
47+
final trimmed = bytes.sublist(0, end);
48+
String decoded;
49+
try {
50+
decoded = utf8.decode(trimmed, allowMalformed: true);
51+
} catch (_) {
52+
return '0x${output.pkh.hex}';
53+
}
54+
55+
if (_isPrintable(decoded)) {
56+
return decoded;
57+
}
58+
59+
return '0x${output.pkh.hex}';
60+
}
61+
62+
bool _isPrintable(String value) {
63+
if (value.contains('\uFFFD')) {
64+
return false;
65+
}
66+
for (final rune in value.runes) {
67+
if (rune == 0x09 || rune == 0x0A || rune == 0x0D) {
68+
continue;
69+
}
70+
if (rune < 0x20 || rune == 0x7F) {
71+
return false;
72+
}
73+
}
74+
return true;
75+
}

lib/util/transactions_list/transaction_utils.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:my_wit_wallet/shared/locator.dart';
55
import 'package:my_wit_wallet/theme/colors.dart';
66
import 'package:my_wit_wallet/theme/extended_theme.dart';
77
import 'package:my_wit_wallet/util/get_localization.dart';
8+
import 'package:my_wit_wallet/util/metadata_utils.dart';
89
import 'package:my_wit_wallet/util/storage/database/account.dart';
910
import 'package:my_wit_wallet/util/storage/database/adapters/transaction_adapter.dart';
1011
import 'package:my_wit_wallet/util/storage/database/wallet.dart';
@@ -308,6 +309,14 @@ class TransactionUtils {
308309
}
309310
}
310311

312+
String? metadata() {
313+
if (vti.type != TransactionType.value_transfer || vti.vtt == null) {
314+
return null;
315+
}
316+
317+
return metadataFromOutputs(vti.vtt!.outputs);
318+
}
319+
311320
Widget buildTransactionValue(label, context) {
312321
final theme = Theme.of(context);
313322
final extendedTheme = theme.extension<ExtendedTheme>()!;

lib/widgets/transaction_details.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ class TransactionDetails extends StatelessWidget {
237237
TransactionUtils transactionUtils = TransactionUtils(vti: transaction);
238238
String label = transactionUtils.getLabel();
239239
String? timelock = transactionUtils.timelock();
240+
String? metadata = transactionUtils.metadata();
240241

241242
return ClosableView(
242243
closeSetting: goToList,
@@ -302,7 +303,14 @@ class TransactionDetails extends StatelessWidget {
302303
label: localization.total,
303304
text: transactionUtils.getTransactionValue().amount,
304305
isContentImportant: true,
305-
isLastItem: timelock == null),
306+
isLastItem: timelock == null && metadata == null),
307+
if (metadata != null)
308+
InfoCopy(
309+
label: localization.metadata,
310+
text: metadata.cropMiddle(18),
311+
infoToCopy: metadata,
312+
isContentImportant: true,
313+
isLastItem: timelock == null),
306314
timelock != null
307315
? InfoElement(
308316
label: localization.timelock,

lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/01_recipient_step.dart

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
77
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
88
import 'package:my_wit_wallet/util/min_amount_unstake.dart';
99
import 'package:my_wit_wallet/util/showTxConnectionError.dart';
10+
import 'package:my_wit_wallet/util/metadata_utils.dart';
11+
import 'package:my_wit_wallet/util/storage/database/adapters/transaction_adapter.dart';
1012
import 'package:my_wit_wallet/util/storage/database/wallet_storage.dart';
1113
import 'package:my_wit_wallet/util/storage/scanned_content.dart';
1214
import 'package:my_wit_wallet/widgets/buttons/icon_btn.dart';
@@ -257,6 +259,9 @@ class RecipientStepState extends State<RecipientStep>
257259
transactionBloc.state.transaction.get(widget.transactionType) != null
258260
? transactionBloc.authorizationString
259261
: null;
262+
String? savedMetadata = isVttTransaction
263+
? _getSavedMetadata(transactionBloc.state.transaction)
264+
: null;
260265

261266
if (savedAddress != null) {
262267
_addressController.text = savedAddress;
@@ -273,6 +278,12 @@ class RecipientStepState extends State<RecipientStep>
273278
setAuthorization(savedAuthorization);
274279
}
275280

281+
if (savedMetadata != null) {
282+
_metadataController.text = savedMetadata;
283+
setMetadata(savedMetadata, validate: false);
284+
showAdvancedSettings = true;
285+
}
286+
276287
transactionBloc.add(ResetTransactionEvent());
277288
}
278289

@@ -294,12 +305,21 @@ class RecipientStepState extends State<RecipientStep>
294305

295306
setMetadata(String value, {bool? validate}) {
296307
_metadata = MetadataInput.dirty(
297-
value: _metadata.value,
308+
value: value,
298309
allowValidation:
299310
validate ?? validationUtils.isFormUnFocus(_formFocusElements()),
300311
);
301312
}
302313

314+
String? _getSavedMetadata(BuildTransaction transaction) {
315+
final vtTransaction = transaction.vtTransaction;
316+
if (vtTransaction == null) {
317+
return null;
318+
}
319+
320+
return metadataFromOutputs(vtTransaction.body.outputs);
321+
}
322+
303323
void nextAction() {
304324
final theme = Theme.of(context);
305325
if (_connectionError) {

lib/widgets/witnet/transactions/value_transfer/create_dialog_box/vtt_builder/03_review_step.dart

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:intl/intl.dart';
44
import 'package:my_wit_wallet/util/allow_biometrics.dart';
55
import 'package:my_wit_wallet/util/extensions/string_extensions.dart';
6+
import 'package:my_wit_wallet/util/metadata_utils.dart';
67
import 'package:my_wit_wallet/util/storage/database/adapters/transaction_adapter.dart';
78
import 'package:my_wit_wallet/widgets/layouts/send_transaction_layout.dart';
89
import 'package:my_wit_wallet/widgets/witnet/transactions/value_transfer/modals/general_error_tx_modal.dart';
@@ -102,6 +103,18 @@ class ReviewStepState extends State<ReviewStep>
102103
return '${state.transaction.getAmount(state.transactionType)} ${WIT_UNIT[WitUnit.Wit]}';
103104
}
104105

106+
String? _getMetadata(TransactionState state) {
107+
if (!isVttTransaction) {
108+
return null;
109+
}
110+
final vtTransaction = state.transaction.vtTransaction;
111+
if (vtTransaction == null) {
112+
return null;
113+
}
114+
115+
return metadataFromOutputs(vtTransaction.body.outputs);
116+
}
117+
105118
@override
106119
Widget build(BuildContext context) {
107120
final theme = Theme.of(context);
@@ -163,6 +176,7 @@ class ReviewStepState extends State<ReviewStep>
163176
state.transaction.hasTimelock(state.transactionType);
164177
String address =
165178
state.transaction.getRecipient(state.transactionType);
179+
String? metadata = _getMetadata(state);
166180
return Padding(
167181
padding: EdgeInsets.only(left: 8, right: 8),
168182
child: Column(
@@ -188,22 +202,32 @@ class ReviewStepState extends State<ReviewStep>
188202
SizedBox(
189203
height: 16,
190204
),
191-
if (showFeeInfo) ..._buildTransactionFeeInfo(context),
205+
if (showFeeInfo)
206+
..._buildTransactionFeeInfo(context,
207+
addBottomSpacing: metadata == null),
208+
if (metadata != null)
209+
InfoCopy(
210+
infoToCopy: metadata,
211+
label: localization.metadata,
212+
text: metadata.cropMiddle(18),
213+
isLastItem: true),
214+
if (metadata != null) SizedBox(height: 16),
192215
]));
193216
},
194217
));
195218
}
196219
}
197220

198-
List<Widget> _buildTransactionFeeInfo(BuildContext context) {
221+
List<Widget> _buildTransactionFeeInfo(BuildContext context,
222+
{bool addBottomSpacing = true}) {
199223
int fee = BlocProvider.of<TransactionBloc>(context).getFee();
200224
return [
201225
InfoElement(
202226
label: localization.fee,
203-
isLastItem: true,
227+
isLastItem: addBottomSpacing,
204228
text:
205229
'${fee.standardizeWitUnits().formatWithCommaSeparator()} ${WIT_UNIT[WitUnit.Wit]}'),
206-
SizedBox(height: 16),
230+
if (addBottomSpacing) SizedBox(height: 16),
207231
];
208232
}
209233

0 commit comments

Comments
 (0)