diff --git a/index.js b/index.js index c065226..76be726 100644 --- a/index.js +++ b/index.js @@ -83,10 +83,6 @@ module.exports = { return /^((02|03)?[a-fA-F0-9]{64}|04[a-fA-F0-9]{128})$/.test(pubkey) // "d2f186a630f5558ba3ede10a4dd0549da5854eab3ed28ee8534350c2535d38b0" } , - isAddress (address) { - return /^[m|t|d|T][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{33}$/.test(address) // && address.length>25 && bs58check.decode(address.slice(1)) && ['A'].indexOf(address[0]>=0)) { - } - , isSignature(signature){ return /^[a-fA-F0-9]{128,144}$/.test(signature) } @@ -304,7 +300,7 @@ module.exports = { return null } , - pubkey2position (pubkey, {coin}={}){ + pubkey2position (pubkey, {coin}={}){ // tic, btc, eth 的 position 都是 20节=40字符的。 coin = my.COIN_LIST.indexOf(coin)>=0?coin:my.COIN if(this.isPubkey(pubkey)){ if (coin==='ETH'){ @@ -322,11 +318,11 @@ module.exports = { return null } , - position2address(position, {coin, netType}={}){ - if (!position) return null + position2address(position, {coin, net, format}={}){ + if (!/^[\da-fA-F]{40}$/.test(position)) return null // 不论 tic, btc, eth,其 position 都是 40字符的。 coin = my.COIN_LIST.indexOf(coin)>=0?coin:my.COIN let address - if (coin==='ETH'){ // 对以太坊,按照 EIP55,把纯位置转换为大小写敏感能自我验证的hex地址 + if (coin==='ETH'){ // 对以太坊,按照 EIP55,把纯位置转换为大小写敏感能自我验证的hex地址。仍然为20节=40符。 position = position.toLowerCase().replace('0x', '') let hash = keccak('keccak256').update(position).digest('hex') address = '0x' @@ -338,9 +334,9 @@ module.exports = { } } return address - }else if (coin === 'BTC'){ // 对比特币,把纯位置转换为大小写敏感能自我验证的bs58check地址 + }else if (coin === 'BTC'){ // 对比特币,把纯位置转换为大小写敏感能自我验证的bs58check地址:先加前缀,再加校验,共25字节,再转base58。得到26~34个字符,大多数34个。 let prefix - switch (netType) { + switch (net) { case 'mainnet': prefix='00'; break; // pubkey hash => 1 case 'mainnetSh': prefix='05'; break; // script hash => 3 case 'testnet': prefix='6f'; break; // testnet pubkey hash => m or n @@ -351,52 +347,60 @@ module.exports = { } address=bs58check.encode(Buffer.from(prefix+position, 'hex')) // wallet import format return address - }else { + }else { // 默认为 TIC。把纯位置转换为大小写敏感能自我验证的 b64u(base64 for url) 地址。 let prefix - switch (netType){ - case 'mainnet': prefix='42'; break; // '42'=>T, '6E'=>m - case 'testnet': prefix='7F'; break; // '7F'=>t - case 'devnet': prefix='5A'; break; // '5A'=>d - default: prefix='42' // 默认暂且为 42,为了兼容已经运行的链。 + switch (net){ + // Base58: https://en.bitcoin.it/wiki/List_of_address_prefixes + // Base64: https://baike.baidu.com/item/base64 + case 'mainnet': prefix='4c'; break; // Base58: 0x42=66 => T, Base64: base64 T=0x13=0b00010011 => 0b010011xx = 0x4c~4f + case 'testnet': prefix='b4'; break; // Base58: 0x7f=127,0x80=128 => t, Base64: t=0x2d=0b00101101 => 0b101101xx = 0xB4~B7 + case 'devnet': prefix='74'; break; // Base58: 0x90 => d, Base 64: d=0x1d=0b00011101 => 0b 011101xx = 0x74~77 + default: prefix='4c' } - address=bs58check.encode(Buffer.from(prefix+position, 'hex')) // wallet import format + let checksum = this.hash(this.hash(position)).slice(0,6) // 添加 checksum 使得能够检测大小写错误。注意,不包含 prefix,这样更纯粹、专注。 +// address = this.hex2eip55(prefix + position + checksum) // 前缀1节,位置20节,校验3节,共24节=48字符(能够完全转化为8个色彩),再转eip55。 + address = this.hex2b64u(prefix + position + checksum) // 实际采用 b64u (named by luk.lu as base 64 for url), 共 32字符。 return address } return null } , + address2position(){ + if (/^0x[\da-fA-F]{40}$/.test(address)){ + return address.toLowerCase() + }else if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{26,34}$/.test(address)){ + let hex = this.b58c2hex(address) + if (hex) { + return hex.slice(2) // 去除网络前缀 + } + }else if (/^[Tt][0-9a-zA-Z\-_]{31}$/.test(address)){ // 格式合法 + let hex = this.b64u2hex(address) + let [all, prefix, position, checksum] = hex.match(/^([\da-fA-F]{2})([\da-fA-F]{40})([\da-fA-F]{6})$/) + if (this.hash(this.hash(position)).slice(0,6) === checksum) { + return position + } + } + return null + } + , + isAddress(address){ + if (/^0x[\da-fA-F]{40}$/.test(address)){ + return 'ETH' + }else if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{26,34}$/.test(address) // 格式合法 + && this.b58c2hex(address)){ // 内容合法 + return 'BTC' + }else if (/^[Ttd][0-9a-zA-Z\-_]{31}$/.test(address)){ // 格式合法 + let b64 = address.replace('-', '+').replace('_', '/') + let hex = Buffer.from(b64, 'base64').toString('hex') + let [all, prefix, position, checksum] = hex.match(/^([\da-fA-F]{2})([\da-fA-F]{40})([\da-fA-F]{6})$/) + return this.hash(this.hash(position)).slice(0,6) === checksum + } + return null + } + , pubkey2address (pubkey, option={}) { // pubkey 应当是string类型 option.coin=my.COIN_LIST.indexOf(option.coin)>=0?option.coin:my.COIN - if (this.isPubkey(pubkey)) { - let h256 = crypto.createHash('sha256').update(Buffer.from(pubkey, 'hex')).digest() - let h160 = crypto.createHash('ripemd160').update(h256).digest('hex') - let prefix - if (option.coin==='TIC'){ - switch (option.netType){ - case 'mainnet': prefix='42'; break; // '42'=>T, '6E'=>m - case 'testnet': prefix='7F'; break; // '7F'=>t - case 'devnet': prefix='5A'; break; // '5A'=>d - default: prefix='42' // 默认暂且为 42,为了兼容已经运行的链。 - } - }else if (option.coin==='BTC'){ - switch (option.netType) { - case 'mainnet': prefix='00'; break; // 1 - case 'testnet': prefix='6f'; break; // m or n - case 'p2sh': prefix='05'; break; // 3 - default: prefix='00' - } - }else if (option.coin==='ETH'){ - // 注意,必须要用非压缩的64字节的公钥的buffer,并去掉开头的 04。 - return '0x' + keccak('keccak256').update(Buffer.from(pubkey.slice(2),'hex')).digest('hex').slice(-40) - // 或 const { keccak256 } = require('ethereumjs-util'); keccak256(Buffer.from(pubkey.slice(2),'hex)).toString('hex').slice(40) - // 或 const { Keccak } = require('sha3'); new Keccak('').update(Bufer.from(pubkey.slice(2),'hex')).digest('hex').slice(-40) - }else { - return null - } - var wifAddress=bs58check.encode(Buffer.from(prefix+h160,'hex')) // wallet import format - return wifAddress - } - return null + return this.position2address(this.pubkey2position(pubkey, option), option) } , secword2seed(secword, pass) { // 遵循bip39的算法。和 ether.HDNode.mnemonic2Seed 结果一样,是64字节的种子。 @@ -624,11 +628,26 @@ module.exports = { } } , + hex2b64u(hex){ + if (/^[0-9a-fA-F]+$/.test(hex)) { + return Buffer.from(hex,'hex').toString('base64').replace(/\+/g,'-').replace(/\//g, '_') + } + return null + } + , + b64u2hex(b64u){ + if (/^[0-9a-zA-Z\-_]+$/.test(b64u)){ + let b64 = b64u.replace(/\-/g, '+').replace(/_/g, '/') + return Buffer.from(b64, 'base64').toString('hex') + } + return null + } + , hex2eip55(hex){ - if (typeof(hex)==='string' && hex.length===64) { + if (/^(0x)?[\da-fA-F]*$/.test(hex)) { hex = hex.toLowerCase().replace('0x', '') let hash = keccak('keccak256').update(hex).digest('hex') - result = '0x' + let result = '' for (var i = 0; i < hex.length; i++) { if (parseInt(hash[i], 16) >= 8) { result += hex[i].toUpperCase() @@ -643,18 +662,23 @@ module.exports = { , // test: https://iancoleman.io/bitcoin-key-compression/ // compress: https://hacpai.com/article/1550844562914 - compressPubkey(uncompressed){ + compressPubkey(uncompressed){ // 把 04xy 的非压缩公钥 转成 02x 或 03x 的压缩公钥 let [all, x, y]=uncompressed.toLowerCase().match(/^04(.{64})(.{64})$/) + let compressed if (/[1,3,5,7,9,b,d,f]$/.test(y)){ - return '03'+x // y为奇数=>前缀03 + compressed = '03'+x // y为奇数=>前缀03 }else{ - return '02'+x // y为偶数=>前缀02 + compressed = '02'+x // y为偶数=>前缀02 } + if (this.decompressPubkey(compressed)===uncompressed) { + return compressed + } + return null // 非压缩公钥有错误。 } , // uncompress: https://stackoverflow.com/questions/17171542/algorithm-for-elliptic-curve-point-compression/53478265#53478265 // https://en.bitcoin.it/wiki/Secp256k1 - decompressPubkey(compressed){ + decompressPubkey(compressed){ // 把 02x 或 03x 的压缩公钥 转成 04xy 的非压缩公钥 // Consts for secp256k1 curve. Adjust accordingly const prime = new BigInt('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 16) // 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1 const pIdent = new BigInt('3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c', 16) // prime.add(1).divide(4); @@ -662,7 +686,7 @@ module.exports = { var x = new BigInt(compressed.substring(2), 16) var y = x.modPow(3, prime).add(7).mod(prime).modPow( pIdent, prime ) // y mod p = +-(x^3 + 7)^((p+1)/4) mod p if( y.mod(2).toJSNumber() !== signY ) { // If the parity doesn't match it's the *other* root - y = prime.subtract( y ) // y = prime - y + y = prime.subtract( y ) // y = prime - y } return '04' + x.toString(16).padStart(64, '0') + y.toString(16).padStart(64, '0') } diff --git a/test-decompress.js b/test-decompress.js deleted file mode 100644 index f7e5b0a..0000000 --- a/test-decompress.js +++ /dev/null @@ -1,38 +0,0 @@ -const bigInt = require("big-integer"); - -// Consts for P256 curve. Adjust accordingly -const two = new bigInt(2), -// 115792089210356248762697446949407573530086143415290314195533631308867097853951 -prime = two.pow(256).subtract( two.pow(224) ).add( two.pow(192) ).add( two.pow(96) ).subtract(1), -b = new bigInt( '41058363725152142129326129780047268409114441015993725554835256314039467401291' ), -// Pre-computed value, or literal -// 28948022302589062190674361737351893382521535853822578548883407827216774463488 -pIdent = prime.add(1).divide(4); - -function pad_with_zeroes(number, length) { - var retval = '' + number; - while (retval.length < length) { - retval = '0' + retval; - } - return retval; -} - -/** - * Point decompress NIST curve - * @param {string} Compressed representation in hex string - * @return {string} Uncompressed representation in hex string - */ -function ECPointDecompress( comp ) { - var signY = new Number(comp[1]) - 2; - var x = new bigInt(comp.substring(2), 16); - // y^2 = x^3 - 3x + b - var y = x.pow(3).subtract( x.multiply(3) ).add( b ).modPow( pIdent, prime ); - // If the parity doesn't match it's the *other* root - if( y.mod(2).toJSNumber() !== signY ) { - // y = prime - y - y = prime.subtract( y ); - } - return '04' + pad_with_zeroes(x.toString(16), 64) + pad_with_zeroes(y.toString(16), 64); -} - -console.log(ECPointDecompress('035d77c1e3eac37f685aeea2ae872c4e7e4d159756e57601db3bcccbc549f360b2')) \ No newline at end of file