From 7f5af7ad4541bc528b71a79d9952f43b78ab4de0 Mon Sep 17 00:00:00 2001 From: BigtoC Date: Thu, 26 Jun 2025 21:59:06 +0800 Subject: [PATCH 1/5] feat: add utility methods for address conversion between hex and bech32 formats --- lib/bip/address/address_conversion.dart | 21 +++++++++++++++++++++ lib/bip/bip.dart | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 lib/bip/address/address_conversion.dart diff --git a/lib/bip/address/address_conversion.dart b/lib/bip/address/address_conversion.dart new file mode 100644 index 0000000..a14c602 --- /dev/null +++ b/lib/bip/address/address_conversion.dart @@ -0,0 +1,21 @@ +import 'package:blockchain_utils/bech32/bech32.dart'; +import 'package:blockchain_utils/hex/hex.dart'; + +/// 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 AddressConversion { + static String hexToBech32(String hexAddress, prefix) { + final cleanHex = hexAddress.startsWith('0x') + ? hexAddress.substring(2) + : hexAddress; + final hexAddressBytes = hex.decode(cleanHex); + return Bech32Encoder.encode(prefix, hexAddressBytes); + } + + static String bech32ToHex(String bech32Address, prefix) { + final decoded = Bech32Decoder.decode(prefix, bech32Address); + return '0x${hex.encode(decoded)}'; + } +} diff --git a/lib/bip/bip.dart b/lib/bip/bip.dart index 76f114b..33a952b 100644 --- a/lib/bip/bip.dart +++ b/lib/bip/bip.dart @@ -40,3 +40,5 @@ export 'mnemonic/mnemonic.dart'; export 'coin_conf/config.dart'; export 'slip/slip.dart'; + +export 'address/address_conversion.dart'; From 34f0624ab3f86ef1e2bf8a3871911fe0e47051d1 Mon Sep 17 00:00:00 2001 From: BigtoC Date: Thu, 26 Jun 2025 21:59:19 +0800 Subject: [PATCH 2/5] test: add unit tests for address conversion --- .../address_conversion_test.dart | 32 +++++++++++++++++++ .../address_conversion/test_vector.dart | 14 ++++++++ 2 files changed, 46 insertions(+) create mode 100644 test/address/address_conversion/address_conversion_test.dart create mode 100644 test/address/address_conversion/test_vector.dart diff --git a/test/address/address_conversion/address_conversion_test.dart b/test/address/address_conversion/address_conversion_test.dart new file mode 100644 index 0000000..41e33eb --- /dev/null +++ b/test/address/address_conversion/address_conversion_test.dart @@ -0,0 +1,32 @@ +import 'package:blockchain_utils/bech32/bech32.dart'; +import 'package:blockchain_utils/bip/address/address_conversion.dart'; +import 'package:test/test.dart'; + +import 'test_vector.dart' show testVector; + +void main() { + test("address conversion test", () { + for (final i in testVector) { + final bech32Address = i["bech32"]!; + final hexAddress = i["hex"]!; + + final prefix = bech32Address.split(Bech32Const.separator)[0]; + + // Convert Bech32 to Hex + final convertedHex = AddressConversion.bech32ToHex(bech32Address, prefix); + expect( + convertedHex, + hexAddress.toLowerCase(), + reason: "Converting $bech32Address, Expected: $hexAddress, but got: $convertedHex" + ); + + // Convert Hex to Bech32 + final convertedBech32 = AddressConversion.hexToBech32(hexAddress, prefix); + expect( + convertedBech32, + bech32Address, + reason: "Converting $hexAddress, Expected: $bech32Address, but got: $convertedBech32" + ); + } + }); +} diff --git a/test/address/address_conversion/test_vector.dart b/test/address/address_conversion/test_vector.dart new file mode 100644 index 0000000..8878a47 --- /dev/null +++ b/test/address/address_conversion/test_vector.dart @@ -0,0 +1,14 @@ +final List> testVector = [ + { + "bech32": "mantra1z3yty3yswenj4ngk0wg5qmqf25ssr3wfqayuhv", + "hex": "0x1448b2449076672aCD167b91406c09552101C5C9" + }, + { + "bech32": "crc1gwqac243g2z3vryqsev6acq965f9ttwhw9r7vk", + "hex": "0x4381dc2ab14285160c808659aee005d51255add7" + }, + { + "bech32": "plq1l2hvlkqw3jzh9w5kv47gfmxfqqldrvnm50fv6p", + "hex": "0xFaAEcfd80e8c8572bA96657c84eCc9003Ed1b27B" + } +]; From bcaa0e3c8d724634cdbf0f4fd439fc3ff3ae1d7c Mon Sep 17 00:00:00 2001 From: BigtoC Date: Tue, 8 Jul 2025 00:15:23 +0800 Subject: [PATCH 3/5] refactor: remove address conversion files --- lib/bip/address/address_conversion.dart | 21 ------------ lib/bip/bip.dart | 2 -- .../address_conversion_test.dart | 32 ------------------- .../address_conversion/test_vector.dart | 14 -------- 4 files changed, 69 deletions(-) delete mode 100644 lib/bip/address/address_conversion.dart delete mode 100644 test/address/address_conversion/address_conversion_test.dart delete mode 100644 test/address/address_conversion/test_vector.dart diff --git a/lib/bip/address/address_conversion.dart b/lib/bip/address/address_conversion.dart deleted file mode 100644 index a14c602..0000000 --- a/lib/bip/address/address_conversion.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:blockchain_utils/bech32/bech32.dart'; -import 'package:blockchain_utils/hex/hex.dart'; - -/// 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 AddressConversion { - static String hexToBech32(String hexAddress, prefix) { - final cleanHex = hexAddress.startsWith('0x') - ? hexAddress.substring(2) - : hexAddress; - final hexAddressBytes = hex.decode(cleanHex); - return Bech32Encoder.encode(prefix, hexAddressBytes); - } - - static String bech32ToHex(String bech32Address, prefix) { - final decoded = Bech32Decoder.decode(prefix, bech32Address); - return '0x${hex.encode(decoded)}'; - } -} diff --git a/lib/bip/bip.dart b/lib/bip/bip.dart index 33a952b..76f114b 100644 --- a/lib/bip/bip.dart +++ b/lib/bip/bip.dart @@ -40,5 +40,3 @@ export 'mnemonic/mnemonic.dart'; export 'coin_conf/config.dart'; export 'slip/slip.dart'; - -export 'address/address_conversion.dart'; diff --git a/test/address/address_conversion/address_conversion_test.dart b/test/address/address_conversion/address_conversion_test.dart deleted file mode 100644 index 41e33eb..0000000 --- a/test/address/address_conversion/address_conversion_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:blockchain_utils/bech32/bech32.dart'; -import 'package:blockchain_utils/bip/address/address_conversion.dart'; -import 'package:test/test.dart'; - -import 'test_vector.dart' show testVector; - -void main() { - test("address conversion test", () { - for (final i in testVector) { - final bech32Address = i["bech32"]!; - final hexAddress = i["hex"]!; - - final prefix = bech32Address.split(Bech32Const.separator)[0]; - - // Convert Bech32 to Hex - final convertedHex = AddressConversion.bech32ToHex(bech32Address, prefix); - expect( - convertedHex, - hexAddress.toLowerCase(), - reason: "Converting $bech32Address, Expected: $hexAddress, but got: $convertedHex" - ); - - // Convert Hex to Bech32 - final convertedBech32 = AddressConversion.hexToBech32(hexAddress, prefix); - expect( - convertedBech32, - bech32Address, - reason: "Converting $hexAddress, Expected: $bech32Address, but got: $convertedBech32" - ); - } - }); -} diff --git a/test/address/address_conversion/test_vector.dart b/test/address/address_conversion/test_vector.dart deleted file mode 100644 index 8878a47..0000000 --- a/test/address/address_conversion/test_vector.dart +++ /dev/null @@ -1,14 +0,0 @@ -final List> testVector = [ - { - "bech32": "mantra1z3yty3yswenj4ngk0wg5qmqf25ssr3wfqayuhv", - "hex": "0x1448b2449076672aCD167b91406c09552101C5C9" - }, - { - "bech32": "crc1gwqac243g2z3vryqsev6acq965f9ttwhw9r7vk", - "hex": "0x4381dc2ab14285160c808659aee005d51255add7" - }, - { - "bech32": "plq1l2hvlkqw3jzh9w5kv47gfmxfqqldrvnm50fv6p", - "hex": "0xFaAEcfd80e8c8572bA96657c84eCc9003Ed1b27B" - } -]; From b1c5de136c1b03b760e655853bd742e0633a70a0 Mon Sep 17 00:00:00 2001 From: BigtoC Date: Tue, 8 Jul 2025 00:40:54 +0800 Subject: [PATCH 4/5] feat: add utility methods for converting Ethereum addresses between hex and Bech32 formats --- lib/bip/address/eth_addr.dart | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) 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'; + } +} From 546c6b2b4b69fee6055255b1a0d6db7bfd2bae9d Mon Sep 17 00:00:00 2001 From: BigtoC Date: Tue, 8 Jul 2025 00:40:59 +0800 Subject: [PATCH 5/5] test: add unit tests for Ethereum address conversion between hex and Bech32 formats --- test/address/eth/eth_test.dart | 50 ++++++++++++++++++++++++++++++- test/address/eth/test_vector.dart | 16 ++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) 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" + } +]; +