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

vaariance / eip-712 / 15663061800

15 Jun 2025 12:13PM UTC coverage: 84.0%. First build
15663061800

push

github

code-z2
feat: implement EIP-712 structured data signing

- Add core EIP-712 signing functionality with v3/v4 support
- Introduce TypedDataEncoder class for message encoding
- Add domain separator and type validation support
- Implement comprehensive type handling including arrays and custom structs
- Add utilities for hex/bigint conversions and ABI encoding
- Include example usage and update documentation
- Set up CI/CD with coverage reporting

210 of 250 new or added lines in 5 files covered. (84.0%)

210 of 250 relevant lines covered (84.0%)

1.24 hits per line

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

93.86
/lib/src/typed_data.dart
1
import 'dart:convert';
2
import 'dart:typed_data';
3

4
import 'package:freezed_annotation/freezed_annotation.dart';
5
import 'package:wallet/wallet.dart' show EthereumAddress;
6
import 'package:web3dart/web3dart.dart';
7

8
part 'utils.dart';
9
part 'extensions.dart';
10
part 'models.dart';
11
part 'typed_data.freezed.dart';
12
part 'typed_data.g.dart';
13

14
Uint8List hashTypedData({
1✔
15
  required TypedMessage typedData,
16
  required TypedDataVersion version,
17
}) {
18
  final prefix = hexToBytes('1901');
1✔
19
  final domainHash = eip712DomainHash(typedData: typedData, version: version);
1✔
20
  final messageHash = getMessageHash(typedData: typedData, version: version);
1✔
21

22
  final builder = BytesBuilder();
1✔
23
  builder.add(prefix);
1✔
24
  builder.add(domainHash);
1✔
25
  if (messageHash != null) {
26
    builder.add(messageHash);
1✔
27
  }
28

29
  return keccak256(builder.toBytes());
2✔
30
}
31

32
Uint8List eip712DomainHash({
1✔
33
  required TypedMessage typedData,
34
  required TypedDataVersion version,
35
}) {
36
  final MessageTypes domain = MessageTypes.eip712Domain(
1✔
37
    value: typedData.domain,
1✔
38
  );
39
  final domainTypes = {'EIP712Domain': typedData.types['EIP712Domain'] ?? []};
3✔
40
  return hashStruct(
1✔
41
    primaryType: 'EIP712Domain',
42
    data: domain,
43
    types: domainTypes,
44
    version: version,
45
  );
46
}
47

48
Uint8List? getMessageHash({
1✔
49
  required TypedMessage typedData,
50
  required TypedDataVersion version,
51
}) {
52
  final MessageTypes message = MessageTypes.additionalData(
1✔
53
    value: typedData.message,
1✔
54
  );
55
  final isPrimaryType = typedData.primaryType == 'EIP712Domain';
2✔
56
  if (!isPrimaryType) {
57
    return hashStruct(
1✔
58
      primaryType: typedData.primaryType,
1✔
59
      data: message,
60
      types: typedData.types,
1✔
61
      version: version,
62
    );
63
  }
64
  return null;
65
}
66

67
Uint8List hashStruct({
1✔
68
  required String primaryType,
69
  required MessageTypes data,
70
  required Map<String, List<MessageTypeProperty>> types,
71
  required TypedDataVersion version,
72
}) {
73
  final encoder = EIP712Encoder(types: types, version: version);
1✔
74
  final encodedData = encoder.encodeData(primaryType, data);
1✔
75
  return keccak256(encodedData);
1✔
76
}
77

78
class EIP712Encoder {
79
  final Map<String, List<MessageTypeProperty>> types;
80
  final TypedDataVersion version;
81

82
  EIP712Encoder({required this.types, this.version = TypedDataVersion.v4});
1✔
83

84
  Uint8List encodeData(String primaryType, MessageTypes data) {
1✔
85
    final List<String> encodedTypes = ['bytes32'];
1✔
86
    final List<Object> encodedValues = [hashType(primaryType: primaryType)];
2✔
87

88
    for (var field in types[primaryType] ?? <MessageTypeProperty>[]) {
4✔
89
      if (version == TypedDataVersion.v3) {
2✔
90
        continue;
91
      }
92
      final typeValuePair = encodeField(
1✔
93
        name: field.name,
1✔
94
        type: field.type,
1✔
95
        value: data[field.name],
2✔
96
      );
97
      encodedTypes.add(typeValuePair.type);
1✔
98
      encodedValues.add(typeValuePair.value);
1✔
99
    }
100

101
    return encode(encodedTypes, encodedValues);
1✔
102
  }
103

104
  TypeValuePair encodeField({
1✔
105
    required String name,
106
    required String type,
107
    required dynamic value,
108
  }) {
109
    if (types[type] != null) {
2✔
110
      return (
111
        type: 'bytes32',
112
        value:
113
            version == TypedDataVersion.v4 && value == null
2✔
114
                ? bytesToHex(Uint8List(32), include0x: true)
2✔
115
                : keccak256(encodeData(type, MessageTypes.from(value))),
3✔
116
      );
117
    }
118

119
    if (value == null) {
NEW
120
      throw ArgumentError("missing value for field $name of type $type");
×
121
    }
122

123
    if (type == 'address') {
1✔
124
      if (value is String) {
1✔
125
        if (isHex(value)) {
1✔
126
          return (type: 'address', value: EthereumAddress.fromHex(value));
1✔
127
        } else {
NEW
128
          throw ArgumentError(
×
NEW
129
            "value for field $name of type $type is not a valid hexadecimal string",
×
130
          );
131
        }
132
      }
133
      return (type: 'address', value: value);
134
    }
135

136
    if (type == 'bool') {
1✔
137
      final boolVal =
138
          value is bool ? value : (value.toString().toLowerCase() == 'true');
4✔
139
      return (type: 'bool', value: boolVal);
140
    }
141

142
    if (type == 'bytes') {
1✔
143
      if (value is num) {
1✔
144
        value = intToBytes(BigInt.from(value));
2✔
145
      } else if (isHex(value)) {
1✔
146
        value = hexToBytes(value);
1✔
147
      }
148
      return (type: 'bytes32', value: keccak256(value));
1✔
149
    }
150

151
    if (type.startsWith('bytes') && type != 'bytes' && !type.contains('[')) {
3✔
152
      if (value is num) {
1✔
153
        if (value < 0) {
1✔
154
          return (type: 'bytes32', value: Uint8List(32));
1✔
155
        }
NEW
156
        return (type: 'bytes32', value: intToBytes(BigInt.from(value)));
×
157
      } else if (isHex(value)) {
1✔
NEW
158
        return (type: 'bytes32', value: hexToBytes(value));
×
159
      }
160
      return (type: 'bytes32', value: value);
161
    }
162

163
    if ((type.startsWith('uint') || type.startsWith('int')) &&
2✔
164
        !type.contains('[')) {
1✔
165
      final (min, max, parsed) = rangeCheck(
1✔
166
        type: type,
167
        value: value.toString(),
1✔
168
      );
169

170
      if (parsed < min || parsed > max) {
2✔
171
        throw RangeError(
2✔
172
          'Integer value $parsed out of range for $type '
173
          '($min … $max)',
174
        );
175
      }
176

177
      return (type: type, value: parsed);
178
    }
179

180
    if (type == 'string') {
1✔
181
      if (value is num) {
1✔
182
        value = intToBytes(BigInt.from(value));
2✔
183
      } else if (value is List<int>) {
1✔
184
      } else {
185
        value = Uint8List.fromList(utf8.encode(value));
2✔
186
      }
187
      return (type: 'bytes32', value: keccak256(value));
1✔
188
    }
189

190
    if (type.endsWith(']')) {
1✔
191
      if (version == TypedDataVersion.v3) {
2✔
192
        throw ArgumentError(
1✔
193
          'Arrays are unimplemented in encodeData; use V4 extension',
194
        );
195
      }
196
      final parsedType = type.substring(0, type.lastIndexOf('['));
2✔
197
      final typeValuePairs = value.map(
1✔
198
        (item) => encodeField(name: name, type: parsedType, value: item),
2✔
199
      );
200

201
      final typesList =
202
          typeValuePairs.map((pair) => pair.type).cast<String>().toList();
5✔
203
      final valuesList =
204
          typeValuePairs.map((pair) => pair.value).cast<Object>().toList();
5✔
205

206
      return (type: 'bytes32', value: keccak256(encode(typesList, valuesList)));
2✔
207
    }
208

209
    return (type: type, value: value);
210
  }
211

212
  Uint8List hashType({required String primaryType}) {
1✔
213
    final encodedHashType = encodeType(primaryType: primaryType);
1✔
214
    return keccak256(Uint8List.fromList(utf8.encode(encodedHashType)));
3✔
215
  }
216

217
  String encodeType({required String primaryType}) {
1✔
218
    var result = '';
219
    final unsortedDeps = findTypeDependencies(primaryType: primaryType)
1✔
220
      ..removeWhere((element) => element == primaryType);
3✔
221
    final deps = [primaryType, ...List.of(unsortedDeps)..sort()];
3✔
222

223
    for (final type in deps) {
2✔
224
      final children = types[type];
2✔
225
      if (children == null) {
NEW
226
        throw ArgumentError('No type definition for: \$type');
×
227
      }
228

229
      result +=
1✔
230
          "$type(${types[type]!.map((tp) => '${tp.type} ${tp.name}').join(',')})";
9✔
231
    }
232

233
    return result;
234
  }
235

236
  Set<String> findTypeDependencies({
1✔
237
    required String primaryType,
238
    Set<String>? results,
239
  }) {
240
    final RegExp typeRegex = RegExp(r"^\w*", unicode: true);
1✔
241
    final match = typeRegex.stringMatch(primaryType);
1✔
242

243
    if (match == null || match.isEmpty) {
1✔
NEW
244
      throw ArgumentError('Invalid type: $primaryType');
×
245
    }
246

247
    results ??= <String>{};
248
    if (results.contains(match) || !types.containsKey(match)) {
3✔
249
      return results;
250
    }
251

252
    results.add(match);
1✔
253

254
    for (final field in types[match]!) {
4✔
255
      findTypeDependencies(primaryType: field.type, results: results);
2✔
256
    }
257
    return results;
258
  }
259

260
  (BigInt min, BigInt max, BigInt parsed) rangeCheck({
1✔
261
    required String type,
262
    required String value,
263
  }) {
264
    final isUnsigned = type.startsWith('uint');
1✔
265
    final bitSize =
266
        int.tryParse(type.replaceFirst(RegExp(r'^(?:u?int)'), '')) ?? 256;
3✔
267

268
    final parsed = BigInt.parse(value.toString());
2✔
269

270
    // Signed range:  –2^(N–1) … 2^(N–1)–1
271
    // Unsigned range: 0 … 2^N–1
272
    final min = isUnsigned ? BigInt.zero : -(BigInt.one << (bitSize - 1));
5✔
273
    final max =
274
        isUnsigned
275
            ? (BigInt.one << bitSize) - BigInt.one
4✔
276
            : (BigInt.one << (bitSize - 1)) - BigInt.one;
5✔
277

278
    return (min, max, parsed);
279
  }
280
}
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