Line data Source code
1 : import 'dart:convert';
2 : import 'dart:typed_data';
3 : import 'package:walletkit_dart/src/common/logger.dart';
4 : import 'package:walletkit_dart/src/crypto/evm/entities/contract/contract_function_encoding.dart';
5 : import 'package:walletkit_dart/src/crypto/evm/entities/contract/contract_function_decoding.dart';
6 : import 'package:walletkit_dart/src/utils/keccak.dart';
7 : import 'package:walletkit_dart/walletkit_dart.dart';
8 :
9 : sealed class ContractFunction implements ExternalContractFunctionMixin {
10 : final String name;
11 :
12 8 : const ContractFunction({
13 : required this.name,
14 : });
15 :
16 0 : bool get isExternal => !isLocal;
17 :
18 0 : bool get isLocal => this is LocalContractFunctionMixin;
19 :
20 : List<FunctionParam> get parameters;
21 :
22 7 : String get function {
23 39 : final params = parameters.map((e) => e.type.name).join(',');
24 14 : return "$name($params)";
25 : }
26 :
27 7 : Uint8List get functionSelector {
28 14 : final buffer = keccakUtf8(function);
29 7 : return buffer.sublist(0, 4);
30 : }
31 :
32 7 : String get functionSelectorHex {
33 14 : return functionSelector.toHex;
34 : }
35 :
36 0 : ContractFunctionWithValues addValues({
37 : required List<dynamic> values,
38 : }) {
39 0 : assert(values.length == parameters.length, "Provided values are invalid");
40 :
41 0 : final paramsWithValues = <FunctionParamWithValue>[];
42 0 : for (var i = 0; i < parameters.length; i++) {
43 0 : final param = parameters[i];
44 0 : final value = values[i];
45 :
46 : // if (param.type.internalType != value.runtimeType) {
47 : // if (param.type.internalType == BigInt &&
48 : // value.runtimeType.toString() == "_BigIntImpl") {
49 : // } else
50 : // throw Exception(
51 : // "Invalid type for param: ${param.name}. Expected: ${param.type.internalType} Got: ${value.runtimeType}",
52 : // );
53 : // }
54 :
55 0 : final paramWithValue = FunctionParamWithValue.fromParam(param, value);
56 0 : paramsWithValues.add(paramWithValue);
57 : }
58 :
59 : return switch (this) {
60 0 : ErrorContractFunctionMixin() =>
61 0 : throw Exception("Cannot add values to $this"),
62 0 : LocalContractFunctionMixin functionMixin =>
63 0 : LocalContractFunctionWithValues(
64 0 : name: name,
65 : parameters: paramsWithValues,
66 0 : stateMutability: functionMixin.stateMutability,
67 0 : outputTypes: functionMixin.outputTypes,
68 : ),
69 0 : ExternalContractFunctionMixin _ => ExternalContractFunctionWithValues(
70 0 : name: name,
71 : parameters: paramsWithValues,
72 : ),
73 : };
74 : }
75 :
76 4 : static ExternalContractFunction? fromTextSignature({
77 : required String textSignature,
78 : }) {
79 4 : final opening = textSignature.indexOf("(");
80 4 : final closing = textSignature.lastIndexOf(")");
81 :
82 16 : if (opening == -1 || closing == -1) {
83 : return null;
84 : }
85 :
86 : try {
87 4 : final name = textSignature.substring(0, opening);
88 4 : final params_s = extractParams(
89 8 : textSignature.substring(opening + 1, closing),
90 : );
91 4 : final params = [
92 4 : for (final (type_s, name) in params_s)
93 4 : FunctionParam(
94 : name: name,
95 4 : type: FunctionParamType.fromString(type_s),
96 : ),
97 : ];
98 :
99 4 : return ExternalContractFunction(
100 : name: name,
101 : parameters: params,
102 : );
103 : } catch (e, s) {
104 1 : Logger.logError(
105 : e,
106 : s: s,
107 1 : hint: "Failed to parse function text signature: $textSignature",
108 : );
109 : return null;
110 : }
111 : }
112 :
113 1 : Json toJson() {
114 1 : return {
115 1 : "name": name,
116 5 : "parameters": parameters.map((e) => e.toJson()).toList(),
117 : };
118 : }
119 :
120 0 : factory ContractFunction.fromJson(Map json) {
121 : if (json
122 : case {
123 0 : "name": String name,
124 0 : "parameters": List<dynamic> parameters,
125 : }) {
126 0 : return ExternalContractFunction(
127 : name: name,
128 0 : parameters: [
129 0 : for (final param in parameters) FunctionParam.fromJson(param),
130 : ],
131 : );
132 : }
133 0 : if (json case {"name": String name, "data": String data}) {
134 0 : final dataBytes = data.hexToBytes;
135 0 : final timestamp = json["timeStamp"] as int?;
136 : return switch (name) {
137 0 : "Unknown" =>
138 0 : UnknownContractFunction(data: dataBytes, timeStamp: timestamp),
139 0 : "NotDecodable" =>
140 0 : NotDecodableContractFunction(data: dataBytes, timeStamp: timestamp),
141 0 : _ => throw Exception("Invalid json"),
142 : };
143 : }
144 :
145 0 : throw Exception("Invalid json");
146 : }
147 :
148 : ///
149 : /// Try to decode the raw data using the [abiList]
150 : /// If the function is not found locally it will try to fetch the function from an external source (4byte.directory)
151 : /// If the function is not found, return null
152 : /// If a local function is found, return a [LocalContractFunctionWithValues]
153 : /// If an external function is found, return a [ExternalContractFunctionWithValues]
154 : ///
155 1 : static Future<ContractFunctionWithValues> decodeRawWithFetch({
156 : required Uint8List data,
157 : Map<String, String>? functionMap,
158 : bool openChain = true,
159 : bool fourByte = true,
160 : }) async {
161 2 : if (data.length < 4) return UnknownContractFunction(data: data);
162 :
163 : if (functionMap != null) {
164 1 : final localResult = decodeRaw(data: data, functionMap: functionMap);
165 :
166 1 : if (localResult is! UnknownContractFunction &&
167 0 : localResult is! NotDecodableContractFunction) {
168 : return localResult;
169 : }
170 : }
171 :
172 2 : final function_selector = data.sublist(0, 4).toHex;
173 :
174 : try {
175 2 : final externalFunction = await FunctionSelectorRepository().fetchSelector(
176 : function_selector,
177 : openChain: openChain,
178 : fourByte: fourByte,
179 : );
180 : if (externalFunction == null) {
181 0 : return UnknownContractFunction(data: data);
182 : }
183 :
184 1 : return decode(data: data, function: externalFunction);
185 : } catch (e) {
186 0 : return UnknownContractFunction(data: data);
187 : }
188 : }
189 :
190 : ///
191 : /// Try to decode the raw data using the [abiList] and return the decoded function
192 : /// If the function is not found, return null
193 : ///
194 2 : static ContractFunctionWithValues decodeRaw({
195 : required Uint8List data,
196 : required Map<String, String> functionMap,
197 : }) {
198 4 : if (data.length < 4) return UnknownContractFunction(data: data);
199 :
200 6 : final hex_signature = "0x${data.sublist(0, 4).toHex}";
201 :
202 2 : final text_signarure = functionMap[hex_signature];
203 :
204 1 : if (text_signarure == null) return UnknownContractFunction(data: data);
205 :
206 : final function =
207 1 : ContractFunction.fromTextSignature(textSignature: text_signarure);
208 :
209 0 : if (function == null) return UnknownContractFunction(data: data);
210 :
211 1 : return ContractFunction.decode(
212 : data: data,
213 : function: function,
214 : );
215 : }
216 :
217 : ///
218 : /// Try to decode the raw data using the [function] and return the decoded function
219 : /// If the decoding of the data with information from the [function] fails a [NotDecodableContractFunction] is returned
220 : ///
221 4 : static ContractFunctionWithValues decode({
222 : required Uint8List data,
223 : required ContractFunction function,
224 : }) {
225 : try {
226 8 : if (data.length < 4) {
227 0 : throw FunctionDecodingException("Invalid data length: ${data.length}");
228 : }
229 :
230 8 : final function_selector = data.sublist(0, 4).toHex;
231 :
232 8 : if (function_selector != function.functionSelectorHex) {
233 1 : throw FunctionSelectorException(
234 1 : "Invalid function selector: $function_selector",
235 : );
236 : }
237 :
238 4 : var dataWithoutSelector = data.sublist(4);
239 :
240 12 : if (dataWithoutSelector.length % 32 != 0) {
241 1 : dataWithoutSelector = dataWithoutSelector.sublist(
242 : 0,
243 4 : dataWithoutSelector.length - (dataWithoutSelector.length % 32),
244 : );
245 : }
246 :
247 4 : final decodedParams = decodeDataField(
248 : data: dataWithoutSelector,
249 4 : params: function.parameters,
250 : );
251 :
252 4 : if (function is LocalContractFunction) {
253 1 : return LocalContractFunctionWithValues(
254 1 : name: function.name,
255 : parameters: decodedParams,
256 1 : stateMutability: function.stateMutability,
257 1 : outputTypes: function.outputTypes,
258 : );
259 : }
260 :
261 3 : return ExternalContractFunctionWithValues(
262 3 : name: function.name,
263 : parameters: decodedParams,
264 : );
265 : } catch (e) {
266 1 : return NotDecodableContractFunction(
267 : data: data,
268 : error: e,
269 : );
270 : }
271 : }
272 : }
273 :
274 : sealed class ContractFunctionWithValues extends ContractFunction {
275 7 : const ContractFunctionWithValues({
276 : required super.name,
277 : });
278 :
279 : @override
280 : List<FunctionParamWithValue> get parameters;
281 :
282 3 : Uint8List buildDataField() {
283 3 : final dataFieldBuilder = DataFieldBuilder.fromFunction(function: this);
284 3 : return dataFieldBuilder.buildDataField();
285 : }
286 :
287 1 : factory ContractFunctionWithValues.fromJson(Map json) {
288 : if (json
289 : case {
290 2 : "name": String name,
291 2 : "parameters": List<dynamic> parameters,
292 : }) {
293 1 : return ExternalContractFunctionWithValues(
294 : name: name,
295 1 : parameters: [
296 1 : for (final param in parameters)
297 1 : FunctionParamWithValue.fromJson(param),
298 : ],
299 : );
300 : }
301 0 : if (json case {"name": String name, "data": String data}) {
302 0 : final dataBytes = data.hexToBytes;
303 : return switch (name) {
304 0 : "Unknown" => UnknownContractFunction(data: dataBytes),
305 0 : "NotDecodable" => NotDecodableContractFunction(data: dataBytes),
306 0 : _ => throw Exception("Invalid json"),
307 : };
308 : }
309 :
310 0 : throw Exception("Invalid json");
311 : }
312 : }
313 :
314 : sealed class ContractFunctionWithValuesAndOutputs
315 : extends ContractFunctionWithValues {
316 : List<FunctionParamWithValue> get outputs;
317 :
318 : List<FunctionParamWithValue> get parameters;
319 :
320 3 : const ContractFunctionWithValuesAndOutputs({
321 : required super.name,
322 : });
323 : }
324 :
325 : final class LocalContractFunctionWithValuesAndOutputs
326 : extends ContractFunctionWithValuesAndOutputs
327 : implements LocalContractFunctionMixin {
328 : @override
329 : final List<FunctionParamWithValue> outputs;
330 :
331 0 : @override
332 0 : List<FunctionParam> get outputTypes => outputs;
333 :
334 : @override
335 : final List<FunctionParamWithValue> parameters;
336 :
337 : @override
338 : final StateMutability stateMutability;
339 :
340 3 : factory LocalContractFunctionWithValuesAndOutputs.decode({
341 : required LocalContractFunctionWithValues function,
342 : required Uint8List data,
343 : }) {
344 3 : final decodedOutputs = decodeDataField(
345 : data: data,
346 3 : params: function.outputTypes,
347 : );
348 :
349 3 : return LocalContractFunctionWithValuesAndOutputs(
350 3 : name: function.name,
351 3 : parameters: function.parameters,
352 : outputs: decodedOutputs,
353 3 : stateMutability: function.stateMutability,
354 : );
355 : }
356 :
357 3 : const LocalContractFunctionWithValuesAndOutputs({
358 : required super.name,
359 : required this.outputs,
360 : required this.parameters,
361 : required this.stateMutability,
362 : });
363 : }
364 :
365 : final class ExternalContractFunctionWithValuesAndOutputs
366 : extends ContractFunctionWithValuesAndOutputs
367 : implements ExternalContractFunctionMixin {
368 : @override
369 : final List<FunctionParamWithValue> outputs;
370 :
371 : @override
372 : final List<FunctionParamWithValue> parameters;
373 :
374 : @override
375 : final StateMutability? stateMutability;
376 :
377 : @override
378 : final List<FunctionParam>? outputTypes;
379 :
380 0 : factory ExternalContractFunctionWithValuesAndOutputs.decode({
381 : required ExternalContractFunctionWithValues function,
382 : required List<FunctionParam> outputs,
383 : required Uint8List data,
384 : StateMutability? stateMutability,
385 : }) {
386 0 : final decodedOutputs = decodeDataField(
387 : data: data,
388 : params: outputs,
389 : );
390 :
391 0 : return ExternalContractFunctionWithValuesAndOutputs._(
392 0 : name: function.name,
393 0 : parameters: function.parameters,
394 : outputs: decodedOutputs,
395 : outputTypes: outputs,
396 : stateMutability: stateMutability,
397 : );
398 : }
399 :
400 0 : const ExternalContractFunctionWithValuesAndOutputs._({
401 : required super.name,
402 : required this.outputs,
403 : required this.parameters,
404 : this.stateMutability,
405 : this.outputTypes,
406 : });
407 : }
408 :
409 : ///
410 : /// An Object that represents a contract function where we only have the function signature
411 : /// And hence only have the function name, selector and the parameters
412 : /// Only used for decoding existing datafields
413 : ///
414 : class ExternalContractFunction extends ContractFunction
415 : implements ExternalContractFunctionMixin {
416 : final List<FunctionParam> parameters;
417 :
418 : @override
419 : final StateMutability? stateMutability;
420 :
421 : @override
422 : final List<FunctionParam>? outputTypes;
423 :
424 4 : const ExternalContractFunction({
425 : required this.parameters,
426 : required super.name,
427 : this.stateMutability,
428 : this.outputTypes,
429 : });
430 : }
431 :
432 : ///
433 : /// An Object that represents a contract function where we only have the function signature
434 : /// And hence only have the function name, selector and the parameters
435 : /// Created after decoding the datafield with [ExternalContractFunction]
436 : ///
437 : class ExternalContractFunctionWithValues extends ContractFunctionWithValues
438 : implements ExternalContractFunctionMixin {
439 : @override
440 : final List<FunctionParamWithValue> parameters;
441 :
442 : @override
443 : final StateMutability? stateMutability;
444 :
445 : @override
446 : final List<FunctionParam>? outputTypes;
447 :
448 3 : const ExternalContractFunctionWithValues({
449 : required super.name,
450 : required this.parameters,
451 : this.stateMutability,
452 : this.outputTypes,
453 : });
454 : }
455 :
456 : ///
457 : /// A mixin for Locale Contract Functions
458 : /// Used to get the stateMutability and outputs of the function
459 : /// These are only available in the local contract functions
460 : ///
461 : class LocalContractFunctionMixin implements ExternalContractFunctionMixin {
462 : @override
463 : final StateMutability stateMutability;
464 : @override
465 : final List<FunctionParam> outputTypes;
466 :
467 0 : const LocalContractFunctionMixin({
468 : required this.stateMutability,
469 : required this.outputTypes,
470 : });
471 : }
472 :
473 : ///
474 : /// A mixin for External Contract Functions
475 : /// Does not have any additional properties just a marker
476 : ///
477 : class ExternalContractFunctionMixin {
478 : final StateMutability? stateMutability;
479 : final List<FunctionParam>? outputTypes;
480 :
481 0 : const ExternalContractFunctionMixin({
482 : this.stateMutability,
483 : this.outputTypes,
484 : });
485 : }
486 :
487 : class ErrorContractFunctionMixin extends ExternalContractFunctionMixin {
488 : /// TimeStamp when the error occured
489 : final int timeStamp;
490 :
491 0 : const ErrorContractFunctionMixin({
492 : required this.timeStamp,
493 : });
494 : }
495 :
496 : ///
497 : /// An Object that represents a contract function generated from an ABI
498 : /// Used for Encoding a datafield and after executing decoding the output
499 : ///
500 : class LocalContractFunction extends ContractFunction
501 : implements LocalContractFunctionMixin {
502 : @override
503 : final List<FunctionParam> parameters;
504 : final StateMutability stateMutability;
505 : final List<FunctionParam> outputTypes;
506 :
507 4 : const LocalContractFunction({
508 : required super.name,
509 : required this.parameters,
510 : required this.stateMutability,
511 : required this.outputTypes,
512 : });
513 :
514 4 : @override
515 : LocalContractFunctionWithValues addValues({required List<dynamic> values}) {
516 4 : return LocalContractFunctionWithValues(
517 4 : name: name,
518 4 : parameters: [
519 15 : for (var i = 0; i < parameters.length; i++)
520 12 : FunctionParamWithValue.fromParam(parameters[i], values[i]),
521 : ],
522 4 : stateMutability: stateMutability,
523 4 : outputTypes: outputTypes,
524 : );
525 : }
526 : }
527 :
528 : class LocalContractFunctionWithValues extends ContractFunctionWithValues
529 : implements LocalContractFunctionMixin {
530 : final StateMutability stateMutability;
531 : final List<FunctionParam> outputTypes;
532 :
533 : @override
534 : final List<FunctionParamWithValue> parameters;
535 :
536 4 : const LocalContractFunctionWithValues({
537 : required super.name,
538 : required this.parameters,
539 : required this.stateMutability,
540 : required this.outputTypes,
541 : });
542 : }
543 :
544 : class FunctionSelectorException implements Exception {
545 : final String message;
546 :
547 1 : FunctionSelectorException(this.message);
548 :
549 0 : @override
550 : String toString() {
551 0 : return message;
552 : }
553 : }
554 :
555 : class FunctionDecodingException implements Exception {
556 : final String message;
557 :
558 0 : FunctionDecodingException(this.message);
559 :
560 0 : @override
561 : String toString() {
562 0 : return message;
563 : }
564 : }
565 :
566 : class UnknownContractFunction extends ContractFunctionWithValues
567 : implements ErrorContractFunctionMixin {
568 : final Uint8List data;
569 : final int timeStamp;
570 0 : @override
571 : StateMutability? get stateMutability => null;
572 :
573 0 : @override
574 : List<FunctionParam>? get outputTypes => null;
575 :
576 0 : @override
577 : List<FunctionParamWithValue> get parameters {
578 0 : return [
579 0 : FunctionParamWithValue.fromParam<Uint8List>(
580 0 : FunctionParam(name: "data", type: FunctionParamBytes()),
581 0 : data,
582 : ),
583 : ];
584 : }
585 :
586 1 : UnknownContractFunction({
587 : required this.data,
588 : int? timeStamp,
589 2 : }) : timeStamp = timeStamp ?? DateTime.now().millisecondsSinceEpoch,
590 1 : super(name: "Unknown");
591 :
592 0 : @override
593 : Uint8List get functionSelector {
594 0 : return data.sublist(0, 4);
595 : }
596 :
597 0 : @override
598 : String get functionSelectorHex {
599 0 : return functionSelector.toHex;
600 : }
601 :
602 0 : @override
603 0 : String get function => name;
604 :
605 0 : String get UTF8 {
606 0 : return utf8.decode(data);
607 : }
608 :
609 0 : @override
610 : Json toJson() {
611 0 : return {
612 0 : "name": name,
613 0 : "data": data.toHex,
614 0 : "timeStamp": timeStamp,
615 : };
616 : }
617 : }
618 :
619 : class NotDecodableContractFunction extends ContractFunctionWithValues
620 : implements ErrorContractFunctionMixin {
621 : final Uint8List data;
622 : final Object? error;
623 : final int timeStamp;
624 :
625 0 : @override
626 : StateMutability? get stateMutability => null;
627 :
628 0 : @override
629 : List<FunctionParam>? get outputTypes => null;
630 :
631 0 : @override
632 : List<FunctionParamWithValue> get parameters {
633 0 : return [
634 0 : FunctionParamWithValue.fromParam<Uint8List>(
635 0 : FunctionParam(name: "data", type: FunctionParamBytes()),
636 0 : data,
637 : ),
638 : ];
639 : }
640 :
641 1 : NotDecodableContractFunction({
642 : required this.data,
643 : int? timeStamp,
644 : this.error,
645 2 : }) : timeStamp = timeStamp ?? DateTime.now().millisecondsSinceEpoch,
646 1 : super(name: "NotDecodable");
647 :
648 0 : @override
649 : Uint8List get functionSelector {
650 0 : if (data.length < 4) return Uint8List(0);
651 0 : return data.sublist(0, 4);
652 : }
653 :
654 0 : @override
655 : String get functionSelectorHex {
656 0 : return functionSelector.toHex;
657 : }
658 :
659 0 : @override
660 0 : String get function => name;
661 :
662 0 : String get UTF8 {
663 0 : return utf8.decode(data);
664 : }
665 :
666 0 : @override
667 : Json toJson() {
668 0 : return {
669 0 : "name": name,
670 0 : "data": data.toHex,
671 0 : "timeStamp": timeStamp,
672 : };
673 : }
674 : }
675 :
676 : extension on String {
677 4 : (String type, String? name) splitParam() {
678 4 : final string = trim();
679 :
680 4 : final splitted = string.split(' ');
681 :
682 8 : if (splitted.length != 2) return (string, null);
683 :
684 6 : return (splitted[0], splitted[1]);
685 : }
686 : }
687 :
688 4 : List<(String type, String? name)> extractParams(String text) {
689 4 : text = text.trim();
690 :
691 4 : var opening = text.indexOf("(");
692 :
693 4 : final values = <(String type, String? name)>[];
694 8 : final start = opening == -1
695 : ? text
696 1 : : opening == 0
697 : ? ""
698 2 : : text.substring(0, opening - 1);
699 :
700 4 : if (start.isNotEmpty) {
701 4 : if (start.startsWith('(')) {
702 0 : values.add(start.splitParam());
703 4 : } else if (start.contains(',')) {
704 4 : values.addAll(
705 16 : start.split(',').map((s) => s.splitParam()),
706 : );
707 : } else {
708 6 : values.add(start.splitParam());
709 : }
710 : }
711 8 : if (opening != -1) {
712 1 : var closing = -1;
713 : var nested = 0;
714 3 : for (var i = opening; i < text.length; i++) {
715 1 : final char = text[i];
716 2 : if (nested == 0 && char == ',') {
717 : closing = i;
718 : break;
719 : }
720 :
721 2 : if (char == "(") nested++;
722 2 : if (char == ")") nested--;
723 : }
724 :
725 3 : if (closing == -1) closing = text.length;
726 :
727 2 : var tuple = text.substring(opening, closing).splitParam();
728 1 : values.add(tuple);
729 :
730 2 : if (closing < text.length) {
731 1 : values.addAll(
732 1 : extractParams(
733 2 : text.substring(closing + 1),
734 : ),
735 : );
736 : }
737 : }
738 :
739 : return values;
740 : }
|