LCOV - code coverage report
Current view: top level - utils - address_validation.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 75.5 % 53 40
Test Date: 2025-01-30 01:10:00 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            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              : }
        

Generated by: LCOV version 2.0-1