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 2 : _ when token.isUTXO => validateUTXOAddress(address: address, token: token).$1,
51 1 : tron => validateTronAddress(address: address),
52 1 : _ => validateEVMAddress(address: address),
53 : };
54 : }
55 :
56 0 : (AddressError?, UTXONetworkType?) validateAddressAnyChain({
57 : required String address,
58 : }) {
59 0 : final AddressError? evmError = validateEVMAddress(address: address);
60 : if (evmError == null) {
61 : return (null, null);
62 : }
63 0 : final AddressError? tronError = validateTronAddress(address: address);
64 : if (tronError == null) {
65 : return (null, null);
66 : }
67 0 : return validateUTXOAddress(address: address, token: null);
68 : }
69 :
70 1 : (AddressError?, UTXONetworkType?) validateUTXOAddress({
71 : required String address,
72 : required CoinEntity? token,
73 : }) {
74 4 : if (address.trim().length != address.length) {
75 : return (AddressError.WHITESPACE, null);
76 : }
77 :
78 : try {
79 : // this is the main-check: see if an output-script can be generated
80 2 : P2Hash(address).publicKeyScript;
81 : } catch (e) {
82 1 : if (address.startsWith("0x")) {
83 : return (AddressError.WRONG_CHAIN, null);
84 2 : } else if (e.toString().contains("checksum")) {
85 : return (AddressError.INVALID_CHECKSUM, null);
86 : } else {
87 : return (AddressError.INVALID, null);
88 : }
89 : }
90 :
91 2 : if (token?.isUTXO ?? false) if (!address.startsWithAny(
92 4 : UTXO_Network_List.singleWhereOrNull((net) => net.coin == token)!
93 1 : .addressPrefixes
94 1 : .values
95 1 : .toList())) {
96 : return (AddressError.WRONG_CHAIN, null);
97 : }
98 :
99 1 : final network = UTXO_Network_List.singleWhereOrNull(
100 5 : (net) => address.startsWithAny(net.addressPrefixes.values.toList()),
101 : );
102 :
103 : return (null, network); // successful validation
104 : }
105 :
106 1 : AddressError? validateEVMAddress({required String address}) {
107 4 : if (address.trim().length != address.length) {
108 : return AddressError.WHITESPACE;
109 : }
110 1 : if (!address.startsWith("0x")) {
111 1 : final utxoError = validateUTXOAddress(address: address, token: null).$1;
112 : if (utxoError == null) {
113 : return AddressError.WRONG_CHAIN;
114 : } else {
115 : return AddressError.INVALID;
116 : }
117 : }
118 : try {
119 1 : _validate(address);
120 : return null;
121 : } catch (e) {
122 2 : if (e.toString().contains("not EIP-55 conformant")) {
123 : return AddressError.INVALID_CHECKSUM;
124 : } else {
125 : return AddressError.INVALID;
126 : }
127 : }
128 : }
129 :
130 3 : final RegExp _basicAddress = RegExp(r'^(0x)?[0-9a-f]{40}$', caseSensitive: false);
131 :
132 1 : void _validate(String address) {
133 : // Basic address validation
134 2 : if (!_basicAddress.hasMatch(address)) {
135 1 : throw ArgumentError.value(
136 : address,
137 : 'address',
138 : 'Must be a hex string with a length of 40, optionally prefixed with "0x"',
139 : );
140 : }
141 :
142 2 : final cleanAddress = address.startsWith('0x') ? address.substring(2) : address;
143 :
144 4 : if (cleanAddress.toUpperCase() == cleanAddress || cleanAddress.toLowerCase() == cleanAddress) {
145 : return;
146 : }
147 :
148 : // Perform EIP-55 checksum validation
149 1 : _validateEIP55Checksum(address);
150 : }
151 :
152 1 : void _validateEIP55Checksum(String address) {
153 : // Strip the '0x' prefix if present
154 2 : final cleanAddress = address.startsWith('0x') ? address.substring(2) : address;
155 :
156 : // Convert to lowercase and compute the hash
157 3 : final hash = keccakAscii(cleanAddress.toLowerCase()).toHex;
158 2 : for (var i = 0; i < 40; i++) {
159 2 : final hashedPos = int.parse(hash[i], radix: 16);
160 5 : if ((hashedPos > 7 && cleanAddress[i].toUpperCase() != cleanAddress[i]) ||
161 5 : (hashedPos <= 7 && cleanAddress[i].toLowerCase() != cleanAddress[i])) {
162 2 : throw ArgumentError(
163 : 'Address has invalid case-characters and is'
164 : 'thus not EIP-55 conformant, rejecting. Address was: $address',
165 : );
166 : }
167 : }
168 : }
|