Skip to content

Commit

Permalink
Add support for STX (Blockstack) address (#299)
Browse files Browse the repository at this point in the history
* Add support for RUNE (ThorChain)

* Add support for STX (Blockstack) address

* tslint

* chore: reduce size
  • Loading branch information
vinhbhn authored Sep 3, 2021
1 parent 663a613 commit c2466a9
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 9 additions & 1 deletion src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1016,6 +1016,14 @@ const vectors: Array<TestVector> = [
{ text: 'hs1qd42hrldu5yqee58se4uj6xctm7nk28r70e84vx', hex: '6d5571fdbca1019cd0f0cd792d1b0bdfa7651c7e' },
],
},
{
name: 'STX',
coinType: 5757,
passingVectors: [
{ text: 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7', hex: 'a46ff88886c2ef9762d970b4d2c63678835bd39d71b4ba47' },
{ text: 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G', hex: 'a46ff88886c2ef9762d970b4d2c63678835bd39df7d47410' },
],
},
{
name: 'GO',
coinType: 6060,
Expand Down
197 changes: 197 additions & 0 deletions src/blockstack/stx-c32.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'),
Expand Down

0 comments on commit c2466a9

Please sign in to comment.