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