• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vaariance / web3-signers / #10

15 Jan 2026 03:08PM UTC coverage: 94.705% (-0.01%) from 94.715%
#10

push

code-z2
feat: Rework ABI encoding and parsing, remove EIP-7702 dependency, and update the Signer interface with sync and async methods.

194 of 205 new or added lines in 4 files covered. (94.63%)

5 existing lines in 3 files now uncovered.

948 of 1001 relevant lines covered (94.71%)

2.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.12
/lib/src/utils/abi.dart
1
part of '../../web3_signers.dart';
2

3
/// Decodes a list of ABI-encoded types and values.
4
///
5
/// Parameters:
6
///   - `types`: a list of [AbiParameter] or [String] types or [Map]s describing the ABI types to decode.
7
///   - `value`: A [Bytes] containing the ABI-encoded data to be decoded.
8
///
9
/// Returns:
10
///   A list of decoded values with the specified type.
11
///
12
/// Example:
13
/// ```dart
14
/// var decodedValues = decodeAbiParameters(['uint256', 'string'], encodedData);
15
/// ```
16
List decodeAbiParameters(List<dynamic> types, Bytes value) {
1✔
17
  final parsedTypes =
18
      types.map((t) {
2✔
19
        if (t is String) return t;
1✔
20
        if (t is AbiParameter) return t.canonicalType;
2✔
21
        if (t is Dict) return AbiParameter.fromJson(t).canonicalType;
3✔
NEW
22
        throw ArgumentError('Invalid type: $t');
×
23
      }).toList();
1✔
24
  return decode(parsedTypes, value);
1✔
25
}
26

27
/// Encodes a list of types and values into ABI-encoded data.
28
///
29
/// Parameters:
30
///   - `types`: a list of [AbiParameter] or [String] types or [Map]s describing the ABI types.
31
///   - `values`: A list of dynamic values to be ABI-encoded.
32
///
33
/// Returns:
34
///   A [Bytes] containing the ABI-encoded types and values.
35
///
36
/// Example:
37
/// ```dart
38
/// var encodedData = encodeAbiParameters(['uint256', 'string'], [BigInt.from(123), 'Hello']);
39
/// ```
40
Bytes encodeAbiParameters(List<dynamic> types, List<dynamic> values) {
2✔
41
  final parsedTypes =
42
      types.map((t) {
4✔
43
        if (t is String) return t;
2✔
44
        if (t is AbiParameter) return t.canonicalType;
2✔
45
        if (t is Dict) return AbiParameter.fromJson(t).canonicalType;
3✔
46
        throw ArgumentError('Invalid type: $t');
2✔
47
      }).toList();
2✔
48
  return encode(parsedTypes, values);
2✔
49
}
50

51
/// Packs a list of heterogeneous values into a single [Bytes] array.
52
///
53
/// This method does not perform any form of sanity check. hence, all inputs
54
///
55
/// Parameters:
56
///   - `values`: A list of values to pack.
57
///
58
/// Returns:
59
///   A single [Bytes] instance containing the concatenated byte
60
///   representation of every supplied value.
61
///
62
/// Example:
63
/// ```dart
64
/// final packed = encodePacked([
65
///   BigInt.from(0x42),
66
///   '0xdeadbeef',
67
///   utf8.encode('hello'),
68
/// ]);
69
/// ```
70
Bytes encodePacked(List<dynamic> values) {
1✔
71
  final list = <int>[];
1✔
72

73
  for (var item in values) {
2✔
74
    if (item is BigInt) {
1✔
75
      list.addAll(intToBytes(item));
2✔
76
    } else if (item is Bytes) {
1✔
77
      list.addAll(item);
1✔
78
    } else if (item is String) {
1✔
79
      isHex(item)
1✔
80
          ? list.addAll(hexToBytes(item))
2✔
81
          : list.addAll(utf8.encode(item));
2✔
82
    } else if (item is num) {
1✔
83
      list.addAll(intToBytes(BigInt.from(item)));
3✔
84
    } else if (item is List) {
1✔
85
      list.addAll(encodePacked(item));
2✔
86
    } else if (item is _Uint) {
1✔
87
      list.addAll(item.toBytes());
2✔
88
    } else {
89
      throw ArgumentError(
1✔
90
        "Unable to pack provided value. Invalid Type",
91
        item.toString(),
1✔
92
      );
93
    }
94
  }
95

96
  return Bytes.fromList(list);
1✔
97
}
98

99
/// Searches for an ABI item in a list of parsed items.
100
///
101
/// Parameters:
102
///   - `abi`: A list of [AbiItem]s to search through.
103
///   - `name`: The name of the function or event to find.
104
///   - `args`: Optional list of arguments to match the input length.
105
///
106
/// Returns:
107
///   The matching [AbiItem], or `null` if no match is found.
108
///
109
/// Example:
110
/// ```dart
111
/// final output = getAbiItem(abi: uniswapAbi, name: 'swapExactInputForOutput');
112
/// ```
113
AbiItem? getAbiItem({
1✔
114
  required List<AbiItem> abi,
115
  String? name,
116
  List<dynamic>? args,
117
}) {
118
  // Basic matching implementation
2✔
119
  for (final item in abi) {
1✔
120
    if (name != null && item.name != name) continue;
2✔
121
    if (args != null && item.inputs.length != args.length) continue;
4✔
122
    // Could add type checking logic here
123
    return item;
1✔
124
  }
2✔
125
  return null;
1✔
NEW
126
}
×
127

2✔
128
/// Parses a list of human-readable ABI signatures into a list of [AbiItem]s.
129
///
130
/// Parameters:
131
///   - `sources`: A list of human-readable ABI signature strings.
132
///
133
/// Returns:
134
///   A list of [AbiItem] objects representing the parsed signatures.
135
///
136
/// Example:
137
/// ```dart
138
/// final abi = parseAbi([
139
///   'function balanceOf(address owner) view returns (uint256)',
140
///   'event Transfer(address indexed from, address indexed to, uint256 amount)',
141
/// ]);
142
/// ```
143
List<AbiItem> parseAbi(List<String> sources) {
144
  return sources.map((s) => parseAbiItem(s)).toList();
145
}
146

147
/// Parses a generic signature (function, event, error) into an [AbiItem].
148
///
1✔
149
/// Parameters:
4✔
150
///   - `source`: A human-readable ABI signature string.
151
///
152
/// Returns:
153
///   An [AbiItem] object representing the parsed signature.
154
///
155
/// Example:
156
/// ```dart
157
/// final item = parseAbiItem('function foo(uint a) view returns (uint b)');
158
/// ```
159
AbiItem parseAbiItem(String source) {
160
  source = source.trim();
161

162
  String itemType = _extractItemType(source);
163
  String cleanSource = _extractCleanSource(source);
164

1✔
165
  final firstParen = cleanSource.indexOf('(');
1✔
166
  if (firstParen == -1) {
167
    throw FormatException(
1✔
168
      'Invalid signature: expected arguments in parentheses',
1✔
169
    );
170
  }
1✔
171

2✔
NEW
172
  final name = cleanSource.substring(0, firstParen).trim();
×
173
  final inputsEnd = _findInputsEnd(cleanSource, firstParen);
174

175
  final inputsString = cleanSource.substring(firstParen + 1, inputsEnd);
176
  final inputs = parseAbiParameters(inputsString);
177

2✔
178
  String remainder = cleanSource.substring(inputsEnd + 1).trim();
1✔
179
  String modifiersPart = remainder;
180
  void updateModifierPart(String part) {
2✔
181
    modifiersPart = part;
1✔
182
  }
183

3✔
184
  final outputs = _extractOutputs(remainder, itemType, updateModifierPart);
185

1✔
186
  String stateMutability = switch (modifiersPart) {
187
    _ when modifiersPart.contains('view') => 'view',
188
    _ when modifiersPart.contains('pure') => 'pure',
189
    _ when modifiersPart.contains('payable') => 'payable',
1✔
190
    _ => 'nonpayable',
191
  };
192

1✔
193
  if (itemType == 'event' || itemType == 'error') {
1✔
194
    stateMutability = 'view';
1✔
195
  }
196

197
  return AbiItem(
198
    name: name.isEmpty ? null : name,
2✔
199
    type: itemType,
200
    inputs: inputs,
201
    outputs: outputs,
202
    stateMutability: stateMutability,
1✔
203
  );
1✔
204
}
205

206
/// Parses a single ABI parameter string.
207
///
208
/// Supports:
209
/// - Basic types: `uint256`, `address`, `string`, etc.
210
/// - Tuples: `tuple(uint a, uint b)`, `(uint a, uint b)`
211
/// - Arrays: `uint256[]`, `tuple(...)[]`
212
///
213
/// Parameters:
214
///   - `source`: A string representing a single ABI parameter.
215
///
216
/// Returns:
217
///   An [AbiParameter] object.
218
///
219
/// Example:
220
/// ```dart
221
/// final param = parseAbiParameter('uint256 amount');
222
/// ```
223
AbiParameter parseAbiParameter(String source) {
224
  source = source.trim();
225

226
  String type;
227
  String? name;
228
  List<AbiParameter>? components;
1✔
229

1✔
230
  if (source.startsWith('tuple') || source.startsWith('(')) {
231
    final startParen = source.indexOf('(');
232
    final closingParen = _extractClosingParen(startParen, source);
233

234
    final innerContent = source.substring(startParen + 1, closingParen);
235
    components = parseAbiParameters(innerContent);
2✔
236

1✔
237
    // Get the part after the closing parenthesis: e.g., "[] points" or " points"
1✔
238
    String afterType = source.substring(closingParen + 1).trim();
239

2✔
240
    type = _extractType(afterType);
1✔
241
    name = _extractName(afterType);
242
  } else {
243
    // Standard types
3✔
244
    final parts = source.split(RegExp(r'\s+'));
245
    final validParts =
1✔
246
        parts
1✔
247
            .where((p) => !['calldata', 'memory', 'storage'].contains(p))
248
            .toList();
249

2✔
250
    if (validParts.isEmpty) {
251
      throw FormatException('Invalid Abi Parameter: $source');
252
    }
4✔
253

1✔
254
    type = validParts[0];
255

1✔
NEW
256
    if (validParts.length > 1) {
×
257
      if (validParts[1] == 'indexed') {
258
        if (validParts.length > 2) name = validParts[2];
259
      } else {
1✔
260
        name = validParts[1];
261
      }
2✔
262
    }
2✔
263
  }
3✔
264

265
  return AbiParameter(name: name, type: type, components: components);
1✔
266
}
267

268
/// Parses a list of ABI parameters from a string representation.
269
///
270
/// Handles nested tuples and standard ABI types.
1✔
271
///
272
/// Parameters:
273
///   - `source`: A string containing comma-separated ABI parameters.
274
///
275
/// Returns:
276
///   A list of [AbiParameter] objects.
277
///
278
/// Example:
279
/// ```dart
280
/// final params = parseAbiParameters('uint256 amount, (string name, address wallet) user');
281
/// ```
282
List<AbiParameter> parseAbiParameters(String source) {
283
  if (source.trim().isEmpty) return [];
284
  if (source.trim() == '()') return [];
285

286
  // Handle surrounding parentheses if present (common in full usage)
287
  if (source.trim().startsWith('(') && source.trim().endsWith(')')) {
1✔
288
    source = source.trim().substring(1, source.trim().length - 1);
3✔
289
  }
2✔
290

291
  final parameters = <AbiParameter>[];
292
  final parts = _splitParams(source);
4✔
NEW
293

×
294
  for (final part in parts) {
295
    parameters.add(parseAbiParameter(part));
296
  }
1✔
297

1✔
298
  return parameters;
299
}
2✔
300

2✔
301
String _extractCleanSource(String source) {
302
  return switch (source) {
303
    _ when source.startsWith('function ') => source.substring(9).trim(),
304
    _ when source.startsWith('event ') => source.substring(6).trim(),
305
    _ when source.startsWith('error ') => source.substring(6).trim(),
306
    _ when source.startsWith('constructor') => source.substring(11).trim(),
1✔
307
    _ when source.startsWith('fallback') => source.substring(8).trim(),
308
    _ when source.startsWith('receive') => source.substring(7).trim(),
3✔
309
    _ => source,
3✔
310
  };
3✔
311
}
1✔
312

3✔
NEW
313
// Find the closing paren that matches this startParen
×
314
int _extractClosingParen(int startParen, String source) {
315
  int depth = 0;
316
  int closingParen = -1;
317
  for (int i = startParen; i < source.length; i++) {
318
    if (source[i] == '(') {
319
      depth++;
1✔
320
    } else if (source[i] == ')') {
321
      depth--;
1✔
322
    }
3✔
323

2✔
324
    if (depth == 0) {
1✔
325
      closingParen = i;
2✔
326
      break;
1✔
327
    }
328
  }
329

1✔
330
  if (closingParen == -1) {
331
    throw FormatException('Unbalanced parentheses in parameter: $source');
332
  }
333

334
  return closingParen;
335
}
2✔
NEW
336

×
337
String _extractItemType(String source) {
338
  return switch (source) {
339
    _ when source.startsWith('event ') => 'event',
340
    _ when source.startsWith('error ') => 'error',
341
    _ when source.startsWith('constructor') => 'constructor',
342
    _ when source.startsWith('fallback') => 'fallback',
1✔
343
    _ when source.startsWith('receive') => 'receive',
344
    _ => 'function',
1✔
345
  };
1✔
346
}
1✔
347

1✔
348
List<AbiParameter> _extractOutputs(
1✔
349
  String remainder,
350
  String itemType,
351
  Function(String) updateModifierPart,
352
) {
353
  List<AbiParameter> outputs = [];
1✔
354

355
  final returnsIndex = remainder.indexOf('returns');
356
  if (returnsIndex != -1) {
357
    updateModifierPart(remainder.substring(0, returnsIndex).trim());
358

1✔
359
    final afterReturns = remainder.substring(returnsIndex + 7).trim();
360
    if (afterReturns.startsWith('(') && afterReturns.endsWith(')')) {
1✔
361
      final outputInner = afterReturns.substring(1, afterReturns.length - 1);
2✔
362
      outputs = parseAbiParameters(outputInner);
3✔
363
    }
364
  }
3✔
365
  return outputs;
2✔
366
}
3✔
367

1✔
368
int _findInputsEnd(String cleanSource, int firstParen) {
369
  int depth = 0;
370
  int inputsEnd = -1;
371
  for (int i = firstParen; i < cleanSource.length; i++) {
372
    if (cleanSource[i] == '(') {
373
      depth++;
1✔
374
    } else if (cleanSource[i] == ')') {
375
      depth--;
1✔
376
    }
3✔
377

2✔
378
    if (depth == 0) {
1✔
379
      inputsEnd = i;
2✔
380
      break;
1✔
381
    }
382
  }
383
  return inputsEnd;
1✔
384
}
385

386
/// Helper to split comma-separated params ignoring nested parens
387
List<String> _splitParams(String text) {
388
  final res = <String>[];
389
  int depth = 0;
390
  int lastIndex = 0;
391

392
  for (int i = 0; i < text.length; i++) {
1✔
393
    if (text[i] == '(') {
1✔
394
      depth++;
395
    } else if (text[i] == ')') {
396
      depth--;
397
    } else if (text[i] == ',' && depth == 0) {
3✔
398
      res.add(text.substring(lastIndex, i).trim());
2✔
399
      lastIndex = i + 1;
1✔
400
    }
2✔
401
  }
1✔
402
  if (lastIndex < text.length) {
3✔
403
    res.add(text.substring(lastIndex).trim());
3✔
404
  }
1✔
405
  return res.where((s) => s.isNotEmpty).toList();
406
}
407

2✔
408
String _extractType(String afterType) {
3✔
409
  String type;
410
  if (afterType.startsWith('[')) {
4✔
411
    final arrayMatch = RegExp(r'^(\[[0-9]*\])+').firstMatch(afterType);
412
    if (arrayMatch != null) {
413
      type = 'tuple${arrayMatch.group(0)}';
1✔
414
    } else {
415
      type = 'tuple';
1✔
416
    }
2✔
417
  } else {
418
    type = 'tuple';
2✔
419
  }
420
  return type;
421
}
422

423
String? _extractName(String afterType) {
424
  // Strip array suffix if present to find the name
425
  if (afterType.startsWith('[')) {
426
    final arrayMatch = RegExp(r'^(\[[0-9]*\])+').firstMatch(afterType);
427
    if (arrayMatch != null) {
428
      afterType = afterType.substring(arrayMatch.group(0)!.length).trim();
1✔
429
    }
430
  }
1✔
431

2✔
432
  String? name = afterType.isEmpty ? null : afterType;
433
  if (name != null) {
4✔
434
    final nameParts = name.split(' ');
435
    if (nameParts.length > 1 && nameParts[0] == 'indexed') {
436
      name = nameParts.last;
437
    } else if (nameParts.length == 1 && nameParts[0] == 'indexed') {
1✔
438
      name = null;
439
    }
1✔
440
  }
2✔
NEW
441
  return name;
×
442
}
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc