LCOV - code coverage report
Current view: top level - utils - address_validation.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 74.0 % 50 37
Test Date: 2025-06-07 01:20:49 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.0-1