Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for STX (Blockstack) address #299

Merged
merged 5 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1015,6 +1015,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 @@ -1482,6 +1483,7 @@ export const formats: IFormat[] = [
},
getConfig('IOTA', 4218, bs58Encode, bs58Decode),
getConfig('HNS', 5353, hnsAddressEncoder, hnsAddressDecoder),
getConfig('STX', 5757, c32checkEncode, c32checkDecode),
hexChecksumChain('GO', 6060),
getConfig('NULS', 8964, nulsAddressEncoder, nulsAddressDecoder),
bech32Chain('AVAX', 9000, 'avax'),
Expand Down