From b97a9bc617b612e2654435df49dac8780896b1c3 Mon Sep 17 00:00:00 2001 From: neeboo Date: Mon, 10 Jun 2019 21:52:19 +0800 Subject: [PATCH] [feat] update bech32 --- packages/harmony-account/src/account.ts | 11 +- .../harmony-contract/src/methods/method.ts | 3 +- packages/harmony-core/src/blockchain.ts | 22 +- packages/harmony-core/src/harmony.ts | 21 +- packages/harmony-crypto/src/address.ts | 41 ++- packages/harmony-crypto/src/bech32.ts | 234 ++++++++++++++++++ packages/harmony-crypto/src/index.ts | 2 + .../harmony-transaction/src/transaction.ts | 24 +- packages/harmony-utils/src/utils.ts | 6 + packages/harmony-utils/src/validators.ts | 26 ++ 10 files changed, 365 insertions(+), 25 deletions(-) create mode 100644 packages/harmony-crypto/src/bech32.ts diff --git a/packages/harmony-account/src/account.ts b/packages/harmony-account/src/account.ts index f1c1574..dfc5a74 100644 --- a/packages/harmony-account/src/account.ts +++ b/packages/harmony-account/src/account.ts @@ -2,12 +2,13 @@ import { generatePrivateKey, getAddressFromPrivateKey, getPubkeyFromPrivateKey, - toChecksumAddress, + // toChecksumAddress, encrypt, decrypt, EncryptOptions, Keystore, Signature, + getAddress, } from '@harmony-js/crypto'; import { isPrivateKey, add0xToString, hexToNumber } from '@harmony-js/utils'; @@ -49,7 +50,13 @@ class Account { * @return {string} get the checksumAddress */ get checksumAddress(): string { - return this.address ? toChecksumAddress(this.address) : ''; + return this.address ? getAddress(this.address).checksum : ''; + } + get bech32Address(): string { + return this.address ? getAddress(this.address).bech32 : ''; + } + get bech32TestNetAddress(): string { + return this.address ? getAddress(this.address).bech32TestNet : ''; } /** diff --git a/packages/harmony-contract/src/methods/method.ts b/packages/harmony-contract/src/methods/method.ts index b961f40..e97089a 100644 --- a/packages/harmony-contract/src/methods/method.ts +++ b/packages/harmony-contract/src/methods/method.ts @@ -6,6 +6,7 @@ import { } from '@harmony-js/transaction'; import { RPCMethod, getResultForData, Emitter } from '@harmony-js/network'; import { hexToNumber, hexToBN } from '@harmony-js/utils'; +import { getAddress } from '@harmony-js/crypto'; import { AbiItemModel } from '../models/types'; import { Contract } from '../contract'; import { methodEncoder } from '../utils/encoder'; @@ -196,7 +197,7 @@ export class ContractMethod { } const txObject = { ...this.params[0], - to: this.contract.address, + to: getAddress(this.contract.address).checksum, data: this.encodeABI(), }; diff --git a/packages/harmony-core/src/blockchain.ts b/packages/harmony-core/src/blockchain.ts index 63119be..8f1a250 100644 --- a/packages/harmony-core/src/blockchain.ts +++ b/packages/harmony-core/src/blockchain.ts @@ -16,6 +16,8 @@ import { DefaultBlockParams, } from '@harmony-js/utils'; +import { getAddress } from '@harmony-js/crypto'; + import { Transaction } from '@harmony-js/transaction'; class Blockchain extends HarmonyCore { @@ -40,7 +42,7 @@ class Blockchain extends HarmonyCore { * */ @assertObject({ - address: ['isAddress', AssertType.required], + address: ['isValidAddress', AssertType.required], blockNumber: ['isBlockNumber', AssertType.optional], }) async getBalance({ @@ -52,7 +54,7 @@ class Blockchain extends HarmonyCore { }) { const result = await this.messenger.send( RPCMethod.GetBalance, - [address, blockNumber], + [getAddress(address).checksum, blockNumber], this.chainPrefix, ); return this.getRpcResult(result); @@ -201,7 +203,7 @@ class Blockchain extends HarmonyCore { * */ @assertObject({ - address: ['isAddress', AssertType.required], + address: ['isValidAddress', AssertType.required], blockNumber: ['isBlockNumber', AssertType.optional], }) async getCode({ @@ -213,7 +215,7 @@ class Blockchain extends HarmonyCore { }) { const result = await this.messenger.send( RPCMethod.GetCode, - [address, blockNumber], + [getAddress(address).checksum, blockNumber], this.chainPrefix, ); return this.getRpcResult(result); @@ -226,7 +228,7 @@ class Blockchain extends HarmonyCore { } @assertObject({ - address: ['isAddress', AssertType.required], + address: ['isValidAddress', AssertType.required], position: ['isHex', AssertType.required], blockNumber: ['isBlockNumber', AssertType.optional], }) @@ -241,14 +243,14 @@ class Blockchain extends HarmonyCore { }) { const result = await this.messenger.send( RPCMethod.GetStorageAt, - [address, position, blockNumber], + [getAddress(address).checksum, position, blockNumber], this.chainPrefix, ); return this.getRpcResult(result); } @assertObject({ - address: ['isAddress', AssertType.required], + address: ['isValidAddress', AssertType.required], blockNumber: ['isBlockNumber', AssertType.optional], }) async getTransactionCount({ @@ -260,7 +262,7 @@ class Blockchain extends HarmonyCore { }) { const result = await this.messenger.send( RPCMethod.GetTransactionCount, - [address, blockNumber], + [getAddress(address).checksum, blockNumber], this.chainPrefix, ); return this.getRpcResult(result); @@ -305,13 +307,13 @@ class Blockchain extends HarmonyCore { } @assertObject({ - to: ['isAddress', AssertType.optional], + to: ['isValidAddress', AssertType.optional], data: ['isHex', AssertType.optional], }) async estimateGas({ to, data }: { to: string; data: string }) { const result = await this.messenger.send( RPCMethod.EstimateGas, - [{ to, data }], + [{ to: getAddress(to).checksum, data }], this.chainPrefix, ); return this.getRpcResult(result); diff --git a/packages/harmony-core/src/harmony.ts b/packages/harmony-core/src/harmony.ts index 18d2976..8711cca 100644 --- a/packages/harmony-core/src/harmony.ts +++ b/packages/harmony-core/src/harmony.ts @@ -7,6 +7,8 @@ import { ContractFactory, Contract } from '@harmony-js/contract'; import { Wallet, Account } from '@harmony-js/account'; import { Blockchain } from './blockchain'; +const defaultUrl = 'http://localhost:9500'; + export class Harmony extends utils.HarmonyCore { Modules = { HttpProvider, @@ -29,16 +31,21 @@ export class Harmony extends utils.HarmonyCore { private provider: HttpProvider | WSProvider; constructor( url: string, - chainType: utils.ChainType = utils.ChainType.Harmony, - chainId: utils.ChainID = utils.ChainID.Default, + { + chainUrl = defaultUrl, + chainType = utils.ChainType.Harmony, + chainId = utils.ChainID.Default, + }: { chainUrl: string; chainType: utils.ChainType; chainId: utils.ChainID }, ) { super(chainType, chainId); - this.provider = utils.isHttp(url) - ? new HttpProvider(url) - : utils.isWs(url) - ? new WSProvider(url) - : new HttpProvider('http://localhost:9128'); + const providerUrl = url || chainUrl; + + this.provider = utils.isHttp(providerUrl) + ? new HttpProvider(providerUrl) + : utils.isWs(providerUrl) + ? new WSProvider(providerUrl) + : new HttpProvider(defaultUrl); this.messenger = new Messenger(this.provider, this.chainType, this.chainId); this.blockchain = new Blockchain(this.messenger); this.transactions = new TransactionFactory(this.messenger); diff --git a/packages/harmony-crypto/src/address.ts b/packages/harmony-crypto/src/address.ts index cdb7d6d..80a291c 100644 --- a/packages/harmony-crypto/src/address.ts +++ b/packages/harmony-crypto/src/address.ts @@ -1,5 +1,11 @@ +import { + isAddress, + isBech32Address, + isBech32TestNetAddress, +} from '@harmony-js/utils'; + import { toChecksumAddress } from './keyTool'; -import { isAddress } from '@harmony-js/utils'; +import { fromBech32, toBech32, HRP, tHRP } from './bech32'; export class HarmonyAddress { // static validator @@ -14,14 +20,33 @@ export class HarmonyAddress { return toTest.raw === toTest.checksum; } + // static validator + static isValidBech32(str: string) { + const toTest = new HarmonyAddress(str); + return toTest.raw === toTest.bech32; + } + // static validator + static isValidBech32TestNet(str: string) { + const toTest = new HarmonyAddress(str); + return toTest.raw === toTest.bech32TestNet; + } + raw: string; basic: string; get basicHex() { return `0x${this.basic}`; } get checksum() { - return toChecksumAddress(this.basic); + return toChecksumAddress(`0x${this.basic}`); } + + get bech32() { + return toBech32(this.basic, HRP); + } + get bech32TestNet() { + return toBech32(this.basic, tHRP); + } + constructor(raw: string) { this.raw = raw; this.basic = this.getBasic(this.raw); @@ -29,11 +54,23 @@ export class HarmonyAddress { private getBasic(addr: string) { const basicBool = isAddress(addr); + const bech32Bool = isBech32Address(addr); + const bech32TestNetBool = isBech32TestNetAddress(addr); if (basicBool) { return addr.replace('0x', '').toLowerCase(); } + if (bech32Bool) { + const fromB32 = fromBech32(addr, HRP); + return fromB32.replace('0x', '').toLowerCase(); + } + + if (bech32TestNetBool) { + const fromB32TestNet = fromBech32(addr, tHRP); + return fromB32TestNet.replace('0x', '').toLowerCase(); + } + throw new Error(`${addr} is valid address format`); } } diff --git a/packages/harmony-crypto/src/bech32.ts b/packages/harmony-crypto/src/bech32.ts new file mode 100644 index 0000000..c6f734c --- /dev/null +++ b/packages/harmony-crypto/src/bech32.ts @@ -0,0 +1,234 @@ +import { isAddress } from '@harmony-js/utils'; + +import { toChecksumAddress } from './keyTool'; +// This code is taken from https://github.com/sipa/bech32/tree/bdc264f84014c234e908d72026b7b780122be11f/ref/javascript +// Copyright (c) 2017 Pieter Wuille +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; +const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + +const polymod = (values: Buffer): number => { + let chk = 1; + // tslint:disable-next-line + for (let p = 0; p < values.length; ++p) { + const top = chk >> 25; + chk = ((chk & 0x1ffffff) << 5) ^ values[p]; + for (let i = 0; i < 5; ++i) { + if ((top >> i) & 1) { + chk ^= GENERATOR[i]; + } + } + } + return chk; +}; + +const hrpExpand = (hrp: string): Buffer => { + const ret = []; + let p; + for (p = 0; p < hrp.length; ++p) { + ret.push(hrp.charCodeAt(p) >> 5); + } + ret.push(0); + for (p = 0; p < hrp.length; ++p) { + ret.push(hrp.charCodeAt(p) & 31); + } + return Buffer.from(ret); +}; + +function verifyChecksum(hrp: string, data: Buffer) { + return polymod(Buffer.concat([hrpExpand(hrp), data])) === 1; +} + +function createChecksum(hrp: string, data: Buffer) { + const values = Buffer.concat([ + Buffer.from(hrpExpand(hrp)), + data, + Buffer.from([0, 0, 0, 0, 0, 0]), + ]); + // var values = hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]); + const mod = polymod(values) ^ 1; + const ret = []; + for (let p = 0; p < 6; ++p) { + ret.push((mod >> (5 * (5 - p))) & 31); + } + return Buffer.from(ret); +} + +export const bech32Encode = (hrp: string, data: Buffer) => { + const combined = Buffer.concat([data, createChecksum(hrp, data)]); + let ret = hrp + '1'; + // tslint:disable-next-line + for (let p = 0; p < combined.length; ++p) { + ret += CHARSET.charAt(combined[p]); + } + return ret; +}; + +export const bech32Decode = (bechString: string) => { + let p; + let hasLower = false; + let hasUpper = false; + for (p = 0; p < bechString.length; ++p) { + if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) { + return null; + } + if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) { + hasLower = true; + } + if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) { + hasUpper = true; + } + } + if (hasLower && hasUpper) { + return null; + } + bechString = bechString.toLowerCase(); + const pos = bechString.lastIndexOf('1'); + if (pos < 1 || pos + 7 > bechString.length || bechString.length > 90) { + return null; + } + const hrp = bechString.substring(0, pos); + const data = []; + for (p = pos + 1; p < bechString.length; ++p) { + const d = CHARSET.indexOf(bechString.charAt(p)); + if (d === -1) { + return null; + } + data.push(d); + } + + if (!verifyChecksum(hrp, Buffer.from(data))) { + return null; + } + + return { hrp, data: Buffer.from(data.slice(0, data.length - 6)) }; +}; + +// HRP is the human-readable part of zilliqa bech32 addresses +export const HRP = 'hmy'; +export const tHRP = 'thmy'; + +/** + * convertBits + * + * groups buffers of a certain width to buffers of the desired width. + * + * For example, converts byte buffers to buffers of maximum 5 bit numbers, + * padding those numbers as necessary. Necessary for encoding Ethereum-style + * addresses as bech32 ones. + * + * @param {Buffer} data + * @param {number} fromWidth + * @param {number} toWidth + * @param {boolean} pad + * @returns {Buffer|null} + */ +export const convertBits = ( + data: Buffer, + fromWidth: number, + toWidth: number, + pad: boolean = true, +) => { + let acc = 0; + let bits = 0; + const ret = []; + const maxv = (1 << toWidth) - 1; + // tslint:disable-next-line + for (let p = 0; p < data.length; ++p) { + const value = data[p]; + if (value < 0 || value >> fromWidth !== 0) { + return null; + } + acc = (acc << fromWidth) | value; + bits += fromWidth; + while (bits >= toWidth) { + bits -= toWidth; + ret.push((acc >> bits) & maxv); + } + } + + if (pad) { + if (bits > 0) { + ret.push((acc << (toWidth - bits)) & maxv); + } + } else if (bits >= fromWidth || (acc << (toWidth - bits)) & maxv) { + return null; + } + + return Buffer.from(ret); +}; + +/** + * toBech32Address + * + * bech32Encodes a canonical 20-byte Ethereum-style address as a bech32 zilliqa + * address. + * + * The expected format is zil1
where address and checksum + * are the result of bech32 encoding a Buffer containing the address bytes. + * + * @param {string} 20 byte canonical address + * @returns {string} 38 char bech32 bech32Encoded zilliqa address + */ +export const toBech32 = (address: string, useHRP: string = HRP): string => { + if (!isAddress(address)) { + throw new Error('Invalid address format.'); + } + + const addrBz = convertBits( + Buffer.from(address.replace('0x', ''), 'hex'), + 8, + 5, + ); + + if (addrBz === null) { + throw new Error('Could not convert byte Buffer to 5-bit Buffer'); + } + + return bech32Encode(useHRP, addrBz); +}; + +/** + * fromBech32Address + * + * @param {string} address - a valid Zilliqa bech32 address + * @returns {string} a canonical 20-byte Ethereum-style address + */ +export const fromBech32 = (address: string, useHRP: string = HRP): string => { + const res = bech32Decode(address); + + if (res === null) { + throw new Error('Invalid bech32 address'); + } + + const { hrp, data } = res; + + if (hrp !== useHRP) { + throw new Error(`Expected hrp to be ${useHRP} but got ${hrp}`); + } + + const buf = convertBits(data, 5, 8, false); + + if (buf === null) { + throw new Error('Could not convert buffer to bytes'); + } + + return toChecksumAddress('0x' + buf.toString('hex')); +}; diff --git a/packages/harmony-crypto/src/index.ts b/packages/harmony-crypto/src/index.ts index 6932c7c..2c23cb1 100644 --- a/packages/harmony-crypto/src/index.ts +++ b/packages/harmony-crypto/src/index.ts @@ -9,6 +9,8 @@ export * from './bytes'; export * from './rlp'; export * from './keccak256'; export * from './errors'; +export * from './base58'; +export * from './bech32'; // export types export * from './types'; diff --git a/packages/harmony-transaction/src/transaction.ts b/packages/harmony-transaction/src/transaction.ts index 1299c74..602d262 100644 --- a/packages/harmony-transaction/src/transaction.ts +++ b/packages/harmony-transaction/src/transaction.ts @@ -6,6 +6,8 @@ import { stripZeros, Signature, splitSignature, + getAddress, + HarmonyAddress, } from '@harmony-js/crypto'; import { add0xToString, numberToHex, ChainType } from '@harmony-js/utils'; import { @@ -67,7 +69,9 @@ class Transaction { this.gasPrice = params ? params.gasPrice : new BN(0); this.gasLimit = params ? params.gasLimit : new BN(0); this.shardID = params ? params.shardID : 0; - this.to = params ? params.to : '0x'; + + // this.to= params ? params.to:'0x'; + this.to = params ? this.normalizeAddress(params.to) : '0x'; this.value = params ? params.value : new BN(0); this.data = params ? params.data : '0x'; // chainid should change with different network settings @@ -187,7 +191,8 @@ class Transaction { gasPrice: this.gasPrice || new BN(0), gasLimit: this.gasLimit || new BN(0), shardID: this.shardID || 0, - to: this.to || '0x', + // to: this.to || '0x', + to: this.normalizeAddress(this.to) || '0x', value: this.value || new BN(0), data: this.data || '0x', chainId: this.chainId || 0, @@ -203,7 +208,8 @@ class Transaction { this.gasPrice = params ? params.gasPrice : new BN(0); this.gasLimit = params ? params.gasLimit : new BN(0); this.shardID = params ? params.shardID : 0; - this.to = params ? params.to : '0x'; + // this.to = params ? params.to : '0x'; + this.to = params ? this.normalizeAddress(params.to) : '0x'; this.value = params ? params.value : new BN(0); this.data = params ? params.data : '0x'; this.chainId = params ? params.chainId : 0; @@ -453,5 +459,17 @@ class Transaction { throw error; } } + + normalizeAddress(address: string) { + if ( + HarmonyAddress.isValidChecksum(address) || + HarmonyAddress.isValidBech32(address) || + HarmonyAddress.isValidBech32TestNet(address) + ) { + return getAddress(address).checksum; + } else { + throw new Error(`Address format is not supported`); + } + } } export { Transaction }; diff --git a/packages/harmony-utils/src/utils.ts b/packages/harmony-utils/src/utils.ts index d631268..722a39a 100644 --- a/packages/harmony-utils/src/utils.ts +++ b/packages/harmony-utils/src/utils.ts @@ -10,6 +10,9 @@ import { isPublicKey, isPrivateKey, isAddress, + isBech32Address, + isBech32TestNetAddress, + isValidAddress, isHash, isBlockNumber, } from './validators'; @@ -33,6 +36,9 @@ export const validatorArray: any = { isAddress: [isAddress], isHash: [isHash], isBlockNumber: [isBlockNumber], + isBech32Address: [isBech32Address], + isBech32TestNetAddress: [isBech32TestNetAddress], + isValidAddress: [isValidAddress], }; export function validateArgs( diff --git a/packages/harmony-utils/src/validators.ts b/packages/harmony-utils/src/validators.ts index fbc463e..6750756 100644 --- a/packages/harmony-utils/src/validators.ts +++ b/packages/harmony-utils/src/validators.ts @@ -159,3 +159,29 @@ export const isBlockNumber = (obj: any): boolean => { return isHex(obj) || blockParams.some((val) => val === obj); }; isBlockNumber.validator = 'isBlockNumber'; + +export const isBech32Address = (raw: string): boolean => { + return !!raw.match(/^hmy1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38}/); +}; +isBech32Address.validator = 'isBech32Address'; + +export const isBech32TestNetAddress = (raw: string): boolean => { + return !!raw.match(/^thmy1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38}/); +}; +isBech32TestNetAddress.validator = 'isBech32TestNetAddress'; + +export const isValidAddress = (address: string): boolean => { + if (!isString(address)) { + throw new Error(`${address} is not string`); + } + if ( + isAddress(address) || + isBech32Address(address) || + isBech32TestNetAddress(address) + ) { + return true; + } else { + return false; + } +}; +isValidAddress.validator = 'isValidAddress';