以太坊钱包生成收款码时,有些是直接拿裸地址例如 "0x0728F0...75445F" 生成收款二维码,例如 TrustWallet;还有些是使用 "iban:" 开头的 ICAP 串来生成收款二维码,例如 imToken 1.0。
IBAN 本身是国际上一部分银行间转账使用的账号代码格式,以太坊社区在 IBAN 地址格式上做了一些扩展,用来编码以太坊地址和校验码,用作地址交换使用,叫做 ICAP (Inter exchange Client Address Protocol)。
IBAN 编码看起来很简单,但在实现上字母到数值的转换方法挺 trick 的,需要花一些时间进行理解。为简化理解,下面我拿一个例子来说明整个编码过程。
假设我们已经有了一个以太坊地址:0x730aEA2B39AA2Cf6B24829b3D39dC9a1F9297B88,下面是生成对应 ICAP 地址的过程:
第一步:将原始 16 进制以太坊地址转换成为 36 进制地址:
16 进制 ETH 地址:0x730aEA2B39AA2Cf6B24829b3D39dC9a1F9297B88 36 进制 ETH 地址:DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4
第二步:为 36 进制 ETH 地址拼接上国家码 "XE" 和空校验字符串 "00" 形成 36 进制待校验字串:
36 进制 ETH 地址: DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4 36 进制待校验字串: DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4XE00
第三步:将 36 进制待校验字串逐字符转成 10 进制数字字串:
36 进制待校验字串: DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4XE00 10 进制待校验字串: 1315273521273029152915344143118231017341572926622101234174331400
第四步:将 10 进制大整数对 97 取模,然后用 98 - 模数:
校验码:42 = 98 - 1315273521273029152915344143118231017341572926622101234174331400 % 97
第五步:将校验码替换空校验字符串,然后重新安排 XE** 到地址前,并加上前缀:
36 进制待校验字串: DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4XE00 36 进制已校验字串: DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4XE42 36 进制 IBAN 号: iban:XE42DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4
可以用这个 IBAN 号生成二维码,用支持 iban 地址的 app 扫描二维码验证下是否能解析到正确的 ETH 原始地址。
ICAP 地址生成和校验的实现,可以参考下面这段 Java 代码,可直接用于 Android 客户端:
package com.yangwenbo; import java.math.BigInteger; /** * Ethereum ICAP (Inter exchange Client Address Protocol) Address Converter * Convert Ethereum Address from/to ICAP iban address * * @ref https://github.com/ethereum/wiki/wiki/Inter-exchange-Client-Address * -Protocol-(ICAP) */ public class EthICAP { private static String ICAP_XE_PREFIX = "XE"; private static String IBAN_SCHEME = "iban:"; private static String IBAN_MOD = "97"; /** * Build ICAP iban address from ethereum address. * * @param ethAddress ethereum address * @return ICAP iban address * @example input: 0x730aea2b39aa2cf6b24829b3d39dc9a1f9297b88 * return: iban:XE42DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4 */ public static String buildICAP(String ethAddress) { if (!ethAddress.startsWith("0x") || ethAddress.length() != 42) { throw new IllegalArgumentException("Invalid ethereum address."); } BigInteger ethInt = new BigInteger(ethAddress.substring(2), 16); String base36Addr = ethInt.toString(36).toUpperCase(); String checkAddr = base36Addr + ICAP_XE_PREFIX + "00"; String base10Str = ""; for (Character c : checkAddr.toCharArray()) { base10Str += new BigInteger(c.toString(), 36); } Integer checkSum = 98 - (new BigInteger(base10Str)).mod(new BigInteger(IBAN_MOD)).intValue(); String icapAddress = IBAN_SCHEME + ICAP_XE_PREFIX + checkSum.toString() + base36Addr; return icapAddress; } /** * Decode ethereum address from ICAP iban address * * @param icapAddress ICAP iban address * @return ethereum address * @example input: iban:XE42DFRZLRUTFTFY4EVINAHYF7TQ6MACYH4 * return: 0x730aea2b39aa2cf6b24829b3d39dc9a1f9297b88 */ public static String decodeICAP(String icapAddress) { if (!isValid(icapAddress)) { throw new IllegalArgumentException("Invalid icap address."); } BigInteger ethInt = new BigInteger(icapAddress.substring(9), 36); String base16Addr = ethInt.toString(16).toLowerCase(); return "0x" + base16Addr; } /** * Check ICAP iban address validation * * @param icapAddress ICAP iban address * @return true if valid; false if invalid */ public static boolean isValid(String icapAddress) { if (!icapAddress.startsWith("iban:XE") || icapAddress.length() != 40) { return false; } String base10Str = ""; for (Character c : icapAddress.substring(9).toCharArray()) { base10Str += new BigInteger(c.toString(), 36); } for (Character c : icapAddress.substring(5, 9).toCharArray()) { base10Str += new BigInteger(c.toString(), 36); } Integer checkSum = (new BigInteger(base10Str)).mod(new BigInteger(IBAN_MOD)).intValue(); return checkSum == 1; } }