Line data Source code
1 : import 'dart:typed_data';
2 : import 'package:collection/collection.dart';
3 : import 'package:walletkit_dart/src/crypto/utxo/entities/payments/p2h.dart';
4 : import 'package:walletkit_dart/src/utils/general.dart';
5 : import 'package:walletkit_dart/src/utils/keccak.dart';
6 : import 'package:walletkit_dart/walletkit_dart.dart';
7 : import 'package:convert/convert.dart' as convert;
8 :
9 0 : String toChecksumAddress(String address) {
10 0 : if (!address.startsWith("0x")) {
11 0 : throw ArgumentError("not an EVM address");
12 : }
13 0 : final stripAddress = address.replaceFirst("0x", "").toLowerCase();
14 0 : final Uint8List keccakHash = keccakUtf8(stripAddress);
15 0 : final String keccakHashHex = convert.hex.encode(keccakHash);
16 :
17 : String checksumAddress = "0x";
18 0 : for (var i = 0; i < stripAddress.length; i++) {
19 0 : final bool high = int.parse(keccakHashHex[i], radix: 16) >= 8;
20 0 : checksumAddress += (high ? stripAddress[i].toUpperCase() : stripAddress[i]);
21 : }
22 : return checksumAddress;
23 : }
24 :
25 : enum AddressError {
26 : /// we consider it as an error if addresses contain whitespace
27 : WHITESPACE,
28 :
29 : /// garbage addresses that are not valid on any chain
30 : INVALID,
31 :
32 : /// address that looks valid although it is invalid
33 : INVALID_CHECKSUM,
34 :
35 : /// address is valid on another chain
36 : WRONG_CHAIN,
37 :
38 : /// addresses that are valid, but not yet supported
39 : NOT_SUPPORTED,
40 : }
41 :
42 : /**
43 : * Returns null if the address is valid, otherwise returns an AddressError
44 : */
45 1 : AddressError? validateAddress({
46 : required String address,
47 : required CoinEntity token,
48 : }) {
49 : return switch (token) {
50 1 : _ when token.isUTXO =>
51 1 : validateUTXOAddress(address: address, token: token).$1,
52 1 : tron => validateTronAddress(address: address),
53 1 : _ => validateEVMAddress(address: address),
54 : };
55 : }
56 :
57 0 : (AddressError?, UTXONetworkType?) validateAddressAnyChain({
58 : required String address,
59 : }) {
60 0 : final AddressError? evmError = validateEVMAddress(address: address);
61 : if (evmError == null) {
62 : return (null, null);
63 : }
64 0 : final AddressError? tronError = validateTronAddress(address: address);
65 : if (tronError == null) {
66 : return (null, null);
67 : }
68 0 : return validateUTXOAddress(address: address, token: null);
69 : }
70 :
71 1 : (AddressError?, UTXONetworkType?) validateUTXOAddress({
72 : required String address,
73 : required CoinEntity? token,
74 : }) {
75 4 : if (address.trim().length != address.length) {
76 : return (AddressError.WHITESPACE, null);
77 : }
78 :
79 : try {
80 : // this is the main-check: see if an output-script can be generated
81 2 : P2Hash(address).publicKeyScript;
82 : } catch (e) {
83 1 : if (address.startsWith("0x")) {
84 : return (AddressError.WRONG_CHAIN, null);
85 2 : } else if (e.toString().contains("checksum")) {
86 : return (AddressError.INVALID_CHECKSUM, null);
87 : } else {
88 : return (AddressError.INVALID, null);
89 : }
90 : }
91 :
92 2 : if (token?.isUTXO ?? false) if (!address.startsWithAny(
93 4 : UTXO_Network_List.singleWhereOrNull((net) => net.coin == token)!
94 1 : .addressPrefixes
95 1 : .values
96 1 : .toList())) {
97 : return (AddressError.WRONG_CHAIN, null);
98 : }
99 :
100 1 : final network = UTXO_Network_List.singleWhereOrNull(
101 5 : (net) => address.startsWithAny(net.addressPrefixes.values.toList()),
102 : );
103 :
104 : return (null, network); // successful validation
105 : }
106 :
107 1 : AddressError? validateEVMAddress({required String address}) {
108 4 : if (address.trim().length != address.length) {
109 : return AddressError.WHITESPACE;
110 : }
111 1 : if (!address.startsWith("0x")) {
112 1 : final utxoError = validateUTXOAddress(address: address, token: null).$1;
113 : if (utxoError == null) {
114 : return AddressError.WRONG_CHAIN;
115 : } else {
116 : return AddressError.INVALID;
117 : }
118 : }
119 : try {
120 1 : _validate(address);
121 : return null;
122 : } catch (e) {
123 2 : if (e.toString().contains("not EIP-55 conformant")) {
124 : return AddressError.INVALID_CHECKSUM;
125 : } else {
126 : return AddressError.INVALID;
127 : }
128 : }
129 : }
130 :
131 2 : final RegExp _basicAddress =
132 1 : RegExp(r'^(0x)?[0-9a-f]{40}$', caseSensitive: false);
133 :
134 1 : void _validate(String address) {
135 : // Basic address validation
136 2 : if (!_basicAddress.hasMatch(address)) {
137 1 : throw ArgumentError.value(
138 : address,
139 : 'address',
140 : 'Must be a hex string with a length of 40, optionally prefixed with "0x"',
141 : );
142 : }
143 :
144 : final cleanAddress =
145 2 : address.startsWith('0x') ? address.substring(2) : address;
146 :
147 2 : if (cleanAddress.toUpperCase() == cleanAddress ||
148 2 : cleanAddress.toLowerCase() == cleanAddress) {
149 : return;
150 : }
151 :
152 : // Perform EIP-55 checksum validation
153 1 : _validateEIP55Checksum(address);
154 : }
155 :
156 1 : void _validateEIP55Checksum(String address) {
157 : // Strip the '0x' prefix if present
158 : final cleanAddress =
159 2 : address.startsWith('0x') ? address.substring(2) : address;
160 :
161 : // Convert to lowercase and compute the hash
162 3 : final hash = keccakAscii(cleanAddress.toLowerCase()).toHex;
163 2 : for (var i = 0; i < 40; i++) {
164 2 : final hashedPos = int.parse(hash[i], radix: 16);
165 5 : if ((hashedPos > 7 && cleanAddress[i].toUpperCase() != cleanAddress[i]) ||
166 5 : (hashedPos <= 7 && cleanAddress[i].toLowerCase() != cleanAddress[i])) {
167 2 : throw ArgumentError(
168 : 'Address has invalid case-characters and is'
169 : 'thus not EIP-55 conformant, rejecting. Address was: $address',
170 : );
171 : }
172 : }
173 : }
|