diff --git a/lib/bip/address/eth_addr.dart b/lib/bip/address/eth_addr.dart index 3a53d03..3a4f96d 100644 --- a/lib/bip/address/eth_addr.dart +++ b/lib/bip/address/eth_addr.dart @@ -1,3 +1,5 @@ +import 'package:blockchain_utils/bech32/bech32.dart'; +import 'package:blockchain_utils/hex/hex.dart'; import 'package:blockchain_utils/bip/address/addr_dec_utils.dart'; import 'package:blockchain_utils/bip/address/decoder.dart'; import 'package:blockchain_utils/bip/coin_conf/constant/coins_conf.dart'; @@ -126,3 +128,54 @@ class EthAddrEncoder implements BlockchainAddressEncoder { EthAddrUtils._checksumEncode(addr); } } + +/// Some cosmos-sdk chains integrate EVM modules, which means in the same chain, +/// both bech32 and 0x addresses are supported. +/// Here provides a utility methods to convert between each other. +/// Note: Addresses are convertible if and only if both addresses are derived using the same coin type. +class EthBech32Converter { + /// Encodes an Ethereum address in Bech32 format. + /// This method takes a hexadecimal Ethereum address and a prefix, + /// converts the address to bytes, and then encodes to Bech32-format address. + /// + /// Parameters: + /// - hexAddress: The hexadecimal representation of the Ethereum address. + /// - prefix: The Bech32 prefix to be used for encoding. + /// + /// Returns: + /// A Bech32-encoded address. + /// + /// Throws: + /// - AssertionError: If the length of the cleaned hexadecimal address is not equal to the expected Ethereum address length. + static String ethAddressToBech32(String ethAddress, prefix) { + + final cleanHex = AddrDecUtils.validateAndRemovePrefix( + ethAddress, CoinsConf.ethereum.params.addrPrefix! + ); + assert( + cleanHex.length == EthAddrConst.addrLen, + "Invalid Ethereum address length: ${cleanHex.length}, expected: ${EthAddrConst.addrLen}" + ); + final hexAddressBytes = hex.decode(cleanHex); + return Bech32Encoder.encode(prefix, hexAddressBytes); + } + + /// Decodes a Bech32-encoded address. + /// This method takes a Bech32-encoded address + /// and decodes it to its hexadecimal representation. + /// + /// Parameters: + /// - bech32Address: The Bech32-encoded Ethereum address. + /// + /// Returns: + /// A string representing the Ethereum address. + static String bech32ToEthAddress(String bech32Address, prefix) { + final decoded = Bech32Decoder.decode(prefix, bech32Address); + final hexEncoded = hex.encode(decoded); + assert( + hexEncoded.length == EthAddrConst.addrLen, + "Invalid Ethereum address length: ${hexEncoded.length}, expected: ${EthAddrConst.addrLen}" + ); + return '${CoinsConf.ethereum.params.addrPrefix!}$hexEncoded'; + } +} diff --git a/test/address/eth/eth_test.dart b/test/address/eth/eth_test.dart index de058d8..23bceb3 100644 --- a/test/address/eth/eth_test.dart +++ b/test/address/eth/eth_test.dart @@ -1,9 +1,12 @@ +import 'package:blockchain_utils/bech32/bech32.dart'; +import 'package:blockchain_utils/bip/address/addr_dec_utils.dart'; import 'package:blockchain_utils/bip/address/eth_addr.dart'; +import 'package:blockchain_utils/bip/coin_conf/constant/coins_conf.dart'; import '../../quick_hex.dart'; import 'package:blockchain_utils/utils/utils.dart'; import 'package:test/test.dart'; -import 'test_vector.dart' show testVector; +import 'test_vector.dart' show testVector, convertorTestVector; void main() { test("eth address test", () { @@ -16,4 +19,49 @@ void main() { expect(decode.toHex(), i["decode"]); } }); + + test("eth address convertor test", () { + for (final i in convertorTestVector) { + final bech32Address = i["bech32"]!; + final ethAddress = i["hex"]!; + expect( + AddrDecUtils.validateAndRemovePrefix( + ethAddress, CoinsConf.ethereum.params.addrPrefix! + ).length, + EthAddrConst.addrLen + ); + + final prefix = bech32Address.split(Bech32Const.separator)[0]; + + // Convert Bech32 to Hex + final convertedHex = + EthBech32Converter.bech32ToEthAddress(bech32Address, prefix); + expect(convertedHex, ethAddress.toLowerCase(), + reason: + "Converting $bech32Address, Expected: $ethAddress, but got: $convertedHex"); + + // Convert Hex to Bech32 + final convertedBech32 = + EthBech32Converter.ethAddressToBech32(ethAddress, prefix); + expect(convertedBech32, bech32Address, + reason: + "Converting $ethAddress, Expected: $bech32Address, but got: $convertedBech32"); + } + + final invalidAddressWithWrongLength = + "0x1448b2449076672aCD167b91406c09552101C5"; + expect( + AddrDecUtils.validateAndRemovePrefix( + invalidAddressWithWrongLength, + CoinsConf.ethereum.params.addrPrefix! + ).length, + isNot(EthAddrConst.addrLen) + ); + + ( + () => EthBech32Converter.ethAddressToBech32( + invalidAddressWithWrongLength, "eth"), + throwsA(isA()) + ); + }); } diff --git a/test/address/eth/test_vector.dart b/test/address/eth/test_vector.dart index 144d6c8..63101fa 100644 --- a/test/address/eth/test_vector.dart +++ b/test/address/eth/test_vector.dart @@ -350,3 +350,19 @@ final List> testVector = [ "params": {} } ]; + +final List> convertorTestVector = [ + { + "bech32": "mantra1z3yty3yswenj4ngk0wg5qmqf25ssr3wfqayuhv", + "hex": "0x1448b2449076672aCD167b91406c09552101C5C9" + }, + { + "bech32": "crc1gwqac243g2z3vryqsev6acq965f9ttwhw9r7vk", + "hex": "0x4381dc2ab14285160c808659aee005d51255add7" + }, + { + "bech32": "plq1l2hvlkqw3jzh9w5kv47gfmxfqqldrvnm50fv6p", + "hex": "0xFaAEcfd80e8c8572bA96657c84eCc9003Ed1b27B" + } +]; +