From c2466a9b38eb0418cf583e95d3d6883c3327a7aa Mon Sep 17 00:00:00 2001 From: Vincent <71333634+vinhbhn@users.noreply.github.com> Date: Fri, 3 Sep 2021 16:29:42 +0700 Subject: [PATCH] Add support for STX (Blockstack) address (#299) * Add support for RUNE (ThorChain) * Add support for STX (Blockstack) address * tslint * chore: reduce size --- README.md | 1 + package-lock.json | 5 + package.json | 1 + src/__tests__/index.test.ts | 10 +- src/blockstack/stx-c32.ts | 197 ++++++++++++++++++++++++++++++++++++ src/index.ts | 2 + 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/blockstack/stx-c32.ts diff --git a/README.md b/README.md index fd5fd7d4..9d7d26fd 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ This library currently supports the following cryptocurrencies and address forma - SRM (base58, no check) - STEEM (base58 + ripemd160-checksum) - STRAT (base58check P2PKH and P2SH) + - STX (crockford base32 P2PKH and P2SH + ripemd160-checksum) - SYS (base58check P2PKH and P2SH, and bech32 segwit) - TFUEL (checksummed-hex) - THETA (base58check) diff --git a/package-lock.json b/package-lock.json index 7b3e893b..13b413aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8820,6 +8820,11 @@ "resolved": "https://registry.npmjs.org/js-crc/-/js-crc-0.2.0.tgz", "integrity": "sha1-9yxcdhgXa/91zIEqHO2949jraDk=" }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "js-sha512": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", diff --git a/package.json b/package.json index 1befec26..d7089a23 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "bs58": "^4.0.1", "crypto-addr-codec": "^0.1.7", "js-crc": "^0.2.0", + "js-sha256": "^0.9.0", "js-sha512": "^0.8.0", "nano-base32": "^1.0.1", "ripemd160": "^2.0.2", diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 11b69980..46096e75 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -4,7 +4,7 @@ import { IFormat, formats, formatsByName, formatsByCoinType } from '../index'; interface TestVector { name: string; coinType: number; - passingVectors: Array<{ text: string; hex: string; canonical?: string }>; + passingVectors: Array<{ text: string; hex: string; canonical?: string; }>; } // Ordered by coinType @@ -1016,6 +1016,14 @@ const vectors: Array = [ { text: 'hs1qd42hrldu5yqee58se4uj6xctm7nk28r70e84vx', hex: '6d5571fdbca1019cd0f0cd792d1b0bdfa7651c7e' }, ], }, + { + name: 'STX', + coinType: 5757, + passingVectors: [ + { text: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7', hex: 'a46ff88886c2ef9762d970b4d2c63678835bd39d71b4ba47' }, + { text: 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G', hex: 'a46ff88886c2ef9762d970b4d2c63678835bd39df7d47410' }, + ], + }, { name: 'GO', coinType: 6060, diff --git a/src/blockstack/stx-c32.ts b/src/blockstack/stx-c32.ts new file mode 100644 index 00000000..bc949950 --- /dev/null +++ b/src/blockstack/stx-c32.ts @@ -0,0 +1,197 @@ +// https://en.wikipedia.org/wiki/Base32#Crockford's_Base32 +import { sha256 } from 'js-sha256'; +export const C32_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; +const hex = '0123456789abcdef'; + +function hashSha256(data: Buffer): Buffer { + return Buffer.from(sha256.update(data).digest()) +} + +function c32checksum(dataHex: string): string { + const dataHash = hashSha256(hashSha256(Buffer.from(dataHex, 'hex'))); + const checksum = dataHash.slice(0, 4).toString('hex'); + return checksum; +} + +export function c32checkEncode(data: Buffer): string { + const dataHex = data.toString('hex'); + let hash160hex = dataHex.substring(0, dataHex.length - 8); + if (!hash160hex.match(/^[0-9a-fA-F]{40}$/)) { + throw new Error('Invalid argument: not a hash160 hex string'); + } + + hash160hex = hash160hex.toLowerCase(); + if (hash160hex.length % 2 !== 0) { + hash160hex = `0${hash160hex}`; + } + + // p2pkh: 'P' + // p2sh: 'M' + const version = { p2pkh: 22, p2sh: 20 }; + + const checksumHex = dataHex.slice(-8); + let c32str = ''; + let prefix = ''; + + if (checksumHex === c32checksum(`${version.p2pkh.toString(16)}${hash160hex}`)) { + prefix = 'P'; + c32str = c32encode(`${hash160hex}${checksumHex}`); + } else if ((checksumHex === c32checksum(`${version.p2sh.toString(16)}${hash160hex}`))) { + prefix = 'M'; + c32str = c32encode(`${hash160hex}${checksumHex}`); + } + + return `S${prefix}${c32str}`; +} + +function c32encode(inputHex: string): string { + // must be hex + if (!inputHex.match(/^[0-9a-fA-F]*$/)) { + throw new Error('Not a hex-encoded string'); + } + + if (inputHex.length % 2 !== 0) { + inputHex = `0${inputHex}`; + } + + inputHex = inputHex.toLowerCase(); + + let res = []; + let carry = 0; + for (let i = inputHex.length - 1; i >= 0; i--) { + if (carry < 4) { + // tslint:disable-next-line:no-bitwise + const currentCode = hex.indexOf(inputHex[i]) >> carry; + let nextCode = 0; + if (i !== 0) { + nextCode = hex.indexOf(inputHex[i - 1]); + } + // carry = 0, nextBits is 1, carry = 1, nextBits is 2 + const nextBits = 1 + carry; + // tslint:disable-next-line:no-bitwise + const nextLowBits = nextCode % (1 << nextBits) << (5 - nextBits); + const curC32Digit = C32_ALPHABET[currentCode + nextLowBits]; + carry = nextBits; + res.unshift(curC32Digit); + } else { + carry = 0; + } + } + + let C32leadingZeros = 0; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < res.length; i++) { + if (res[i] !== '0') { + break; + } else { + C32leadingZeros++; + } + } + + res = res.slice(C32leadingZeros); + + const zeroPrefix = Buffer.from(inputHex, 'hex') + .toString() + .match(/^\u0000*/); + const numLeadingZeroBytesInHex = zeroPrefix ? zeroPrefix[0].length : 0; + + for (let i = 0; i < numLeadingZeroBytesInHex; i++) { + res.unshift(C32_ALPHABET[0]); + } + + return res.join(''); +} + +function c32normalize(c32input: string): string { + // must be upper-case + // replace all O's with 0's + // replace all I's and L's with 1's + return c32input.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1'); +} + +export function c32checkDecode(data: string): Buffer { + if (data.length <= 5) { + throw new Error('Invalid c32 address: invalid length'); + } + if (data[0] !== 'S') { + throw new Error('Invalid c32 address: must start with "S"'); + } + + const c32data = c32normalize(data.slice(1)); + const versionChar = c32data[0]; + const version = C32_ALPHABET.indexOf(versionChar); + + let versionHex = version.toString(16); + if (versionHex.length === 1) { + versionHex = `0${versionHex}`; + } + + const dataHex = c32decode(c32data.slice(1)); + const checksum = dataHex.slice(-8); + + if (c32checksum(`${versionHex}${dataHex.substring(0, dataHex.length - 8)}`) !== checksum) { + throw new Error('Invalid c32check string: checksum mismatch'); + } + + return Buffer.from(dataHex, 'hex'); +} + +function c32decode(c32input: string): string { + c32input = c32normalize(c32input); + + // must result in a c32 string + if (!c32input.match(`^[${C32_ALPHABET}]*$`)) { + throw new Error('Not a c32-encoded string'); + } + + const zeroPrefix = c32input.match(`^${C32_ALPHABET[0]}*`); + const numLeadingZeroBytes = zeroPrefix ? zeroPrefix[0].length : 0; + + let res = []; + let carry = 0; + let carryBits = 0; + for (let i = c32input.length - 1; i >= 0; i--) { + if (carryBits === 4) { + res.unshift(hex[carry]); + carryBits = 0; + carry = 0; + } + // tslint:disable-next-line:no-bitwise + const currentCode = C32_ALPHABET.indexOf(c32input[i]) << carryBits; + const currentValue = currentCode + carry; + const currentHexDigit = hex[currentValue % 16]; + carryBits += 1; + // tslint:disable-next-line:no-bitwise + carry = currentValue >> 4; + // tslint:disable-next-line:no-bitwise + if (carry > 1 << carryBits) { + throw new Error('Panic error in decoding.'); + } + res.unshift(currentHexDigit); + } + // one last carry + res.unshift(hex[carry]); + + if (res.length % 2 === 1) { + res.unshift('0'); + } + + let hexLeadingZeros = 0; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < res.length; i++) { + if (res[i] !== '0') { + break; + } else { + hexLeadingZeros++; + } + } + + res = res.slice(hexLeadingZeros - (hexLeadingZeros % 2)); + + let hexStr = res.join(''); + for (let i = 0; i < numLeadingZeroBytes; i++) { + hexStr = `00${hexStr}`; + } + + return hexStr; +} diff --git a/src/index.ts b/src/index.ts index 8f73414e..aeb079bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ import { crc32 } from 'js-crc'; import { sha512_256 } from 'js-sha512'; import { decode as nanoBase32Decode, encode as nanoBase32Encode } from 'nano-base32'; import { Keccak, SHA3 } from 'sha3'; +import { c32checkDecode, c32checkEncode } from './blockstack/stx-c32'; import { decode as cborDecode, encode as cborEncode, TaggedValue } from './cbor/cbor'; import { filAddrDecoder, filAddrEncoder } from './filecoin/index'; import { ChainID, isValidAddress } from './flow/index'; @@ -1504,6 +1505,7 @@ export const formats: IFormat[] = [ }, iotaBech32Chain('IOTA', 4218, 'iota'), getConfig('HNS', 5353, hnsAddressEncoder, hnsAddressDecoder), + getConfig('STX', 5757, c32checkEncode, c32checkDecode), hexChecksumChain('GO', 6060), getConfig('NULS', 8964, nulsAddressEncoder, nulsAddressDecoder), bech32Chain('AVAX', 9000, 'avax'),