@gandlaf21/bc-ur
Version:
A JS implementation of the Uniform Resources (UR) specification from Blockchain Commons
150 lines (120 loc) • 5.67 kB
text/typescript
import { Buffer } from "buffer";
import { getCRCHex, partition, split } from "./utils";
const bytewords = 'ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlimplionlistlogoloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolposepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzinczonezoom';
let bytewordsLookUpTable: number[] = [];
const BYTEWORDS_NUM = 256;
const BYTEWORD_LENGTH = 4;
const MINIMAL_BYTEWORD_LENGTH = 2;
enum STYLES {
STANDARD = 'standard',
URI = 'uri',
MINIMAL = 'minimal'
}
const getWord = (index: number): string => {
return bytewords.slice(index * BYTEWORD_LENGTH, (index * BYTEWORD_LENGTH) + BYTEWORD_LENGTH)
}
const getMinimalWord = (index: number): string => {
const byteword = getWord(index);
return `${byteword[0]}${byteword[BYTEWORD_LENGTH - 1]}`
}
const addCRC = (string: string): string => {
const crc = getCRCHex(Buffer.from(string, 'hex'));
return `${string}${crc}`;
}
const encodeWithSeparator = (word: string, separator: string): string => {
const crcAppendedWord = addCRC(word);
const crcWordBuff = Buffer.from(crcAppendedWord, 'hex');
const result = crcWordBuff.reduce((result: string[], w) => ([...result, getWord(w)]), []);
return result.join(separator);
}
const encodeMinimal = (word: string): string => {
const crcAppendedWord = addCRC(word);
const crcWordBuff = Buffer.from(crcAppendedWord, 'hex');
const result = crcWordBuff.reduce((result, w) => result + getMinimalWord(w), '');
return result;
}
const decodeWord = (word: string, wordLength: number): string => {
if (word.length!==wordLength) {
throw new Error("'Invalid Bytewords: word.length does not match wordLength provided'");
}
const dim = 26;
// Since the first and last letters of each Byteword are unique,
// we can use them as indexes into a two-dimensional lookup table.
// This table is generated lazily.
if (bytewordsLookUpTable.length === 0) {
const array_len = dim * dim;
bytewordsLookUpTable = [...new Array(array_len)].map(() => -1)
for (let i = 0; i < BYTEWORDS_NUM; i++) {
const byteword = getWord(i);
let x = byteword[0].charCodeAt(0) - 'a'.charCodeAt(0);
let y = byteword[3].charCodeAt(0) - 'a'.charCodeAt(0);
let offset = y * dim + x;
bytewordsLookUpTable[offset] = i;
}
}
// If the coordinates generated by the first and last letters are out of bounds,
// or the lookup table contains -1 at the coordinates, then the word is not valid.
let x = (word[0]).toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0);
let y = (word[wordLength == 4 ? 3 : 1]).toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0);
if(!(0 <= x && x < dim && 0 <= y && y < dim)){
throw new Error("Invalid Bytewords: invalid word");
}
let offset = y * dim + x;
let value = bytewordsLookUpTable[offset];
if (value === -1) {
throw new Error("Invalid Bytewords: value not in lookup table");
}
// If we're decoding a full four-letter word, verify that the two middle letters are correct.
if (wordLength == BYTEWORD_LENGTH) {
const byteword = getWord(value)
let c1 = word[1].toLowerCase();
let c2 = word[2].toLowerCase();
if (!(c1 === byteword[1] && c2 === byteword[2])) {
throw new Error("Invalid Bytewords: invalid middle letters of word");
}
}
// Successful decode.
return Buffer.from([value]).toString('hex')
}
const _decode = (string: string, separator: string, wordLength: number): string => {
const words = wordLength == BYTEWORD_LENGTH ? string.split(separator) : partition(string, 2)
const decodedString = words.map((word: string) => decodeWord(word, wordLength)).join('');
if (decodedString.length < 5) {
throw new Error("Invalid Bytewords: invalid decoded string length");
}
const [body, bodyChecksum] = split(Buffer.from(decodedString, 'hex'), 4)
const checksum = getCRCHex(body)// convert to hex
if (checksum !== bodyChecksum.toString('hex')) {
throw new Error("Invalid Checksum");
}
return body.toString('hex');
}
const decode = (string: string, style: STYLES = STYLES.MINIMAL): string => {
switch (style) {
case STYLES.STANDARD:
return _decode(string, ' ', BYTEWORD_LENGTH);
case STYLES.URI:
return _decode(string, '-', BYTEWORD_LENGTH);
case STYLES.MINIMAL:
return _decode(string, '', MINIMAL_BYTEWORD_LENGTH);
default:
throw new Error(`Invalid style ${style}`)
}
}
const encode = (string: string, style: STYLES = STYLES.MINIMAL): string => {
switch (style) {
case STYLES.STANDARD:
return encodeWithSeparator(string, ' ');
case STYLES.URI:
return encodeWithSeparator(string, '-');
case STYLES.MINIMAL:
return encodeMinimal(string);
default:
throw new Error(`Invalid style ${style}`)
}
}
export default {
decode,
encode,
STYLES
}