Refactor and simplify code (#8)

* Remove dependency on x-address-codec

* Version 3.0.0
This commit is contained in:
Elliot Lee
2019-09-03 10:51:10 -07:00
parent 1eae464868
commit 0c8d997958
12 changed files with 2567 additions and 124 deletions

View File

@@ -1,7 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- '0.12' - 6
- 8
- 9
script: script:
- npm test --coverage - yarn compile
- npm run codecov - yarn test
- npm run lint - yarn lint

View File

@@ -1,4 +1,4 @@
# ripple-address-codec [![NPM](https://img.shields.io/npm/v/ripple-address-codec.svg)](https://npmjs.org/package/ripple-address-codec) [![Build Status](https://img.shields.io/travis/ripple/ripple-address-codec/master.svg)](https://travis-ci.org/ripple/ripple-address-codec) [![codecov.io](http://codecov.io/github/ripple/ripple-address-codec/coverage.svg?branch=master)](http://codecov.io/github/ripple/ripple-address-codec?branch=master) # ripple-address-codec [![NPM](https://img.shields.io/npm/v/ripple-address-codec.svg)](https://npmjs.org/package/ripple-address-codec)
## API ## API
@@ -34,66 +34,3 @@
112, 112,
114 ] 114 ]
``` ```
## And ?? There's more to the wonderful world than ripple
We give you the kitchen sink.
```js
> console.log(api)
{ Codec: [Function: AddressCodec],
codecs:
{ bitcoin:
{ alphabet: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
codec: [Object],
base: 58 },
ripple:
{ alphabet: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz',
codec: [Object],
base: 58 },
tipple:
{ alphabet: 'RPShNAF39wBUDnEGHJKLM4pQrsT7VWXYZ2bcdeCg65jkm8ofqi1tuvaxyz',
codec: [Object],
base: 58 },
stellar:
{ alphabet: 'gsphnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCr65jkm8oFqi1tuvAxyz',
codec: [Object],
base: 58 } },
decode: [Function: decode],
encode: [Function: encode],
decodeEdSeed: [Function],
encodeEdSeed: [Function],
isValidEdSeed: [Function],
decodeSeed: [Function],
encodeSeed: [Function],
isValidSeed: [Function],
decodeAccountID: [Function],
encodeAccountID: [Function],
isValidAccountID: [Function],
decodeAddress: [Function],
encodeAddress: [Function],
isValidAddress: [Function],
decodeNodePublic: [Function],
encodeNodePublic: [Function],
isValidNodePublic: [Function],
decodeNodePrivate: [Function],
encodeNodePrivate: [Function],
isValidNodePrivate: [Function],
decodeK256Seed: [Function],
encodeK256Seed: [Function],
isValidK256Seed: [Function] }
```
# Decode a bip32 bitcoin public key
```js
var pubVersion = [0x04, 0x88, 0xB2, 0x1E];
var options = {version: pubVersion, alphabet: 'bitcoin'};
var key = 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5e4cp9LB';
var decoded = api.decode(key, options);
var reencoded = api.encode(decoded, options);
console.log(key);
// 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5e4cp9LB'
console.log(reencoded);
// 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5e4cp9LB'
```

View File

@@ -1,33 +1,32 @@
{ {
"name": "ripple-address-codec", "name": "ripple-address-codec",
"version": "2.0.2", "version": "3.0.0",
"description": "encodes/decodes base58 encoded ripple identifiers", "description": "encodes/decodes base58 encoded ripple identifiers",
"main": "src/index.js", "main": "dist/index.js",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"create-hash": "^1.1.2", "base-x": "3.0.4",
"x-address-codec": "^0.7.2" "create-hash": "^1.1.2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/ripple/ripple-address-codec.git" "url": "git://github.com/ripple/ripple-address-codec.git"
}, },
"scripts": { "scripts": {
"test": "istanbul test _mocha", "compile": "tsc",
"prepublish": "npm test && npm run lint", "test": "tsc && istanbul test _mocha",
"codecov": "cat ./coverage/coverage.json | ./node_modules/codecov.io/bin/codecov.io.js", "lint": "tslint -p ./"
"lint": "if ! [ -f eslintrc ]; then curl -o eslintrc 'https://raw.githubusercontent.com/ripple/javascript-style-guide/master/eslintrc'; fi; eslint -c eslintrc src/*.js test/*.js"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^10.12.0",
"codecov.io": "^0.1.6", "codecov.io": "^0.1.6",
"coveralls": "~2.11.4", "coveralls": "~2.11.4",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-plugin-no-unused-expressions": "^0.1.0", "eslint-plugin-no-unused-expressions": "^0.1.0",
"istanbul": "~0.3.21", "istanbul": "~0.3.21",
"mocha": "^5.0.5" "mocha": "^5.0.5",
}, "tslint": "^5.11.0",
"readmeFilename": "README.md", "tslint-eslint-rules": "^5.4.0",
"engines": { "typescript": "^3.1.3"
"node": ">=0.12.0"
} }
} }

View File

@@ -1,33 +0,0 @@
'use strict'
const createHash = require('create-hash')
const apiFactory = require('x-address-codec')
const NODE_PUBLIC = 28
const NODE_PRIVATE = 32
const ACCOUNT_ID = 0
const FAMILY_SEED = 33
const ED25519_SEED = [0x01, 0xE1, 0x4B]
module.exports = apiFactory({
sha256: function(bytes) {
return createHash('sha256').update(new Buffer(bytes)).digest()
},
defaultAlphabet: 'ripple',
codecMethods: {
EdSeed: {
expectedLength: 16,
version: ED25519_SEED
},
Seed: {
// TODO: Use a map, not a parallel array
versionTypes: ['ed25519', 'secp256k1'],
versions: [ED25519_SEED, FAMILY_SEED],
expectedLength: 16
},
AccountID: {version: ACCOUNT_ID, expectedLength: 20},
Address: {version: ACCOUNT_ID, expectedLength: 20},
NodePublic: {version: NODE_PUBLIC, expectedLength: 33},
NodePrivate: {version: NODE_PRIVATE, expectedLength: 32},
K256Seed: {version: FAMILY_SEED, expectedLength: 16}}
})

View File

@@ -0,0 +1 @@
export * from './xrp-codec'

View File

@@ -0,0 +1,59 @@
'use strict'
function seqEqual(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false
}
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) {
return false
}
}
return true
}
function isSequence(val) {
return val.length !== undefined
}
/**
* Concatenates all `arguments` into a single array. Each argument can be either
* a single element or a sequence, which has a `length` property and supports
* element retrieval via sequence[ix].
*
* > concatArgs(1, [2, 3], new Buffer([4,5]), new Uint8Array([6, 7]));
* [1,2,3,4,5,6,7]
*
* @return {Array} - concatenated arguments
*/
function concatArgs() {
const ret = []
const _len = arguments.length
const args = Array(_len)
for (let _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key]
}
args.forEach(function (arg) {
if (isSequence(arg)) {
for (let j = 0; j < arg.length; j++) {
ret.push(arg[j])
}
} else {
ret.push(arg)
}
})
return ret
}
function isSet(o) {
return o !== null && o !== undefined
}
module.exports = {
seqEqual: seqEqual,
concatArgs: concatArgs,
isSet: isSet
}

View File

@@ -0,0 +1,219 @@
/**
* Codec class
*/
const baseCodec = require('base-x')
const {seqEqual, concatArgs} = require('.//utils')
class Codec {
sha256: (bytes: Uint8Array) => Buffer
alphabet: string
codec: any
base: number
constructor(options: {
sha256: (bytes: Uint8Array) => Buffer,
alphabet: string
}) {
this.sha256 = options.sha256
this.alphabet = options.alphabet
this.codec = baseCodec(this.alphabet)
this.base = this.alphabet.length
}
/**
* Encoder.
*
* @param bytes Buffer of data to encode.
* @param opts Options object including the version bytes and the expected length of the data to encode.
*/
encode(bytes: Buffer, opts: {
versions: number[],
expectedLength: number
}) {
const versions = opts.versions
return this.encodeVersioned(bytes, versions, opts.expectedLength)
}
encodeVersioned(bytes: Buffer, versions: number[], expectedLength: number) {
if (expectedLength && bytes.length !== expectedLength) {
throw new Error('unexpected_payload_length: bytes.length does not match expectedLength')
}
return this.encodeChecked(concatArgs(versions, bytes))
}
encodeChecked(buffer: Buffer) {
const check = this.sha256(this.sha256(buffer)).slice(0, 4)
return this.encodeRaw(concatArgs(buffer, check))
}
encodeRaw(bytes: Buffer) {
return this.codec.encode(bytes)
}
/**
* Decoder.
*
* @param base58string Base58Check-encoded string to decode.
* @param opts Options object including the version byte(s) and the expected length of the data after decoding.
*/
decode(base58string: string, opts: {
versions?: (number | number[])[],
expectedLength?: number,
versionTypes?: ['ed25519', 'secp256k1']
} = {}) {
const versions = Array.isArray(opts.versions) ? opts.versions : [opts.versions]
const types = opts.versionTypes
if (versions) {
const withoutSum = this.decodeChecked(base58string)
const ret: {
version: number[] | null,
bytes: Buffer | null,
type: string | null // for seeds, 'ed25519' | 'secp256k1'
} = {
version: null,
bytes: null,
type: null
}
if (versions.length > 1 && !opts.expectedLength) {
throw new Error('expectedLength is required because there are >= 2 possible versions')
}
const versionLengthGuess = typeof versions[0] === 'number' ? 1 : (versions[0] as number[]).length
const payloadLength = opts.expectedLength || withoutSum.length - versionLengthGuess
const versionBytes = withoutSum.slice(0, -payloadLength)
const payload = withoutSum.slice(-payloadLength)
let foundVersion = false
for (let i = 0; i < versions.length; i++) {
const version: number[] = Array.isArray(versions[i]) ? versions[i] as number[] : [versions[i] as number]
if (seqEqual(versionBytes, version)) {
ret.version = version
ret.bytes = payload
if (types) {
ret.type = types[i]
}
foundVersion = true
}
}
if (!foundVersion) {
throw new Error('version_invalid: version bytes do not match any of the provided version(s)')
}
if (opts.expectedLength && ret.bytes.length !== opts.expectedLength) {
throw new Error('unexpected_payload_length: payload length does not match expectedLength')
}
return ret
}
// Assume that base58string is 'checked'
return this.decodeChecked(base58string)
}
decodeChecked(base58string: string) {
const buffer = this.decodeRaw(base58string)
if (buffer.length < 5) {
throw new Error('invalid_input_size: decoded data must have length >= 5')
}
if (!this.verifyCheckSum(buffer)) {
throw new Error('checksum_invalid')
}
return buffer.slice(0, -4)
}
decodeRaw(base58string: string) {
return this.codec.decode(base58string)
}
verifyCheckSum(bytes: Buffer) {
const computed = this.sha256(this.sha256(bytes.slice(0, -4))).slice(0, 4)
const checksum = bytes.slice(-4)
return seqEqual(computed, checksum)
}
}
/**
* XRP codec
*/
const createHash = require('create-hash')
const NODE_PUBLIC = 28
const ACCOUNT_ID = 0
const FAMILY_SEED = 0x21 // 33
const ED25519_SEED = [0x01, 0xE1, 0x4B] // [1, 225, 75]
const codecOptions = {
sha256: function(bytes: Uint8Array) {
return createHash('sha256').update(Buffer.from(bytes)).digest()
},
alphabet: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz'
}
const codecWithXrpAlphabet = new Codec(codecOptions)
// entropy is a Buffer of size 16
// type is 'ed25519' or 'secp256k1'
export function encodeSeed(entropy: Buffer, type: 'ed25519' | 'secp256k1'): string {
if (entropy.length !== 16) {
throw new Error('entropy must have length 16')
}
if (type !== 'ed25519' && type !== 'secp256k1') {
throw new Error('type must be ed25519 or secp256k1')
}
const opts = {
expectedLength: 16,
// for secp256k1, use `FAMILY_SEED`
versions: type === 'ed25519' ? ED25519_SEED : [FAMILY_SEED]
}
// prefixes entropy with version bytes
return codecWithXrpAlphabet.encode(entropy, opts)
}
export function decodeSeed(seed: string, opts: {
versionTypes?: ['ed25519', 'secp256k1'],
versions?: (number | number[])[]
expectedLength?: number
} = {}) {
if (!opts.versionTypes || !opts.versions) {
opts.versionTypes = ['ed25519', 'secp256k1']
opts.versions = [ED25519_SEED, FAMILY_SEED]
}
if (!opts.expectedLength) {
opts.expectedLength = 16
}
return codecWithXrpAlphabet.decode(seed, opts)
}
export function encodeAccountID(bytes: Buffer): string {
const opts = {versions: [ACCOUNT_ID], expectedLength: 20}
return codecWithXrpAlphabet.encode(bytes, opts)
}
export function decodeAccountID(accountId: string): Buffer {
const opts = {versions: [ACCOUNT_ID], expectedLength: 20}
return codecWithXrpAlphabet.decode(accountId, opts).bytes
}
export function decodeNodePublic(base58string: string): Buffer {
const opts = {versions: [NODE_PUBLIC], expectedLength: 33}
return codecWithXrpAlphabet.decode(base58string, opts).bytes
}
export function encodeNodePublic(bytes: Buffer): string {
const opts = {versions: [NODE_PUBLIC], expectedLength: 33}
return codecWithXrpAlphabet.encode(bytes, opts)
}
// Address === AccountID
export function isValidAddress(address: string): boolean {
try {
this.decodeAccountID(address)
} catch (e) {
return false
}
return true
}

View File

@@ -3,7 +3,7 @@
'use strict' 'use strict'
const assert = require('assert') const assert = require('assert')
const api = require('../') const api = require('../dist')
function toHex(bytes) { function toHex(bytes) {
return new Buffer(bytes).toString('hex').toUpperCase() return new Buffer(bytes).toString('hex').toUpperCase()
@@ -15,11 +15,11 @@ function toBytes(hex) {
describe('ripple-address-codec', function() { describe('ripple-address-codec', function() {
function makeTest(type, base58, hex) { function makeTest(type, base58, hex) {
it('can translate between ' + hex + ' and ' + base58, function() { it(`can translate between ${hex} and ${base58} (encode ${type})`, function() {
const actual = api['encode' + type](toBytes(hex)) const actual = api['encode' + type](toBytes(hex))
assert.equal(actual, base58) assert.equal(actual, base58)
}) })
it('can translate between ' + base58 + ' and ' + hex, function() { it(`can translate between ${base58} and ${hex} (decode ${type})`, function() {
const buf = api['decode' + type](base58) const buf = api['decode' + type](base58)
assert.equal(toHex(buf), hex) assert.equal(toHex(buf), hex)
}) })
@@ -33,13 +33,7 @@ describe('ripple-address-codec', function() {
'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH', 'n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH',
'0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828') '0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828')
makeTest('K256Seed', 'sn259rEFXrQrWyx3Q7XneWcwV6dfL', it('can decode arbitrary seeds', function() {
'CF2DE378FBDD7E2EE87D486DFB5A7BFF')
makeTest('EdSeed', 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2',
'4C3A1D213FBDFB14C7C28D609469B341')
it('can decode arbitray seeds', function() {
const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2') const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341') assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341')
assert.equal(decoded.type, 'ed25519') assert.equal(decoded.type, 'ed25519')

View File

@@ -0,0 +1,81 @@
/* eslint-disable no-unused-expressions/no-unused-expressions */
'use strict'
const assert = require('assert')
const api = require('../dist/xrp-codec')
function toHex(bytes) {
return new Buffer(bytes).toString('hex').toUpperCase()
}
function toBytes(hex) {
return new Buffer(hex, 'hex').toJSON().data
}
describe('ripple-address-codec', function() {
describe('encodeSeed', function() {
it('encodes a secp256k1 seed', function() {
const result = api.encodeSeed(toBytes('CF2DE378FBDD7E2EE87D486DFB5A7BFF'), 'secp256k1')
assert.equal(result, 'sn259rEFXrQrWyx3Q7XneWcwV6dfL')
})
it('encodes low secp256k1 seed', function() {
const result = api.encodeSeed(toBytes('00000000000000000000000000000000'), 'secp256k1')
assert.equal(result, 'sp6JS7f14BuwFY8Mw6bTtLKWauoUs')
})
it('encodes high secp256k1 seed', function() {
const result = api.encodeSeed(toBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), 'secp256k1')
assert.equal(result, 'saGwBRReqUNKuWNLpUAq8i8NkXEPN')
})
it('encodes an ed25519 seed', function() {
const result = api.encodeSeed(toBytes('4C3A1D213FBDFB14C7C28D609469B341'), 'ed25519')
assert.equal(result, 'sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
})
it('encodes low ed25519 seed', function() {
const result = api.encodeSeed(toBytes('00000000000000000000000000000000'), 'ed25519')
assert.equal(result, 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE')
})
it('encodes high ed25519 seed', function() {
const result = api.encodeSeed(toBytes('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'), 'ed25519')
assert.equal(result, 'sEdV19BLfeQeKdEXyYA4NhjPJe6XBfG')
})
})
describe('decodeSeed', function() {
it('can decode an Ed25519 seed', function() {
const decoded = api.decodeSeed('sEdTM1uX8pu2do5XvTnutH6HsouMaM2')
assert.equal(toHex(decoded.bytes), '4C3A1D213FBDFB14C7C28D609469B341')
assert.equal(decoded.type, 'ed25519')
})
it('can decode a secp256k1 seed', function() {
const decoded = api.decodeSeed('sn259rEFXrQrWyx3Q7XneWcwV6dfL')
assert.equal(toHex(decoded.bytes), 'CF2DE378FBDD7E2EE87D486DFB5A7BFF')
assert.equal(decoded.type, 'secp256k1')
})
})
describe('encodeAccountID', function() {
it('can encode an AccountID', function() {
const encoded = api.encodeAccountID(toBytes('BA8E78626EE42C41B46D46C3048DF3A1C3C87072'))
assert.equal(encoded, 'rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN')
})
})
describe('decodeNodePublic', function() {
it('can decode a NodePublic', function() {
const decoded = api.decodeNodePublic('n9MXXueo837zYH36DvMc13BwHcqtfAWNJY5czWVbp7uYTj7x17TH')
assert.equal(toHex(decoded), '0388E5BA87A000CB807240DF8C848EB0B5FFA5C8E5A521BC8E105C0F0A44217828')
})
})
})

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"es2017"
],
"outDir": "dist",
"rootDir": "src",
"module": "commonjs",
"moduleResolution": "node",
"strictNullChecks": false,
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"preserveConstEnums": false,
"suppressImplicitAnyIndexErrors": false,
"sourceMap": true,
"skipLibCheck": true,
"allowJs": true
},
"include": [
"src/**/*.ts"
]
}

View File

@@ -0,0 +1,74 @@
{
"extends": [
"tslint-eslint-rules"
],
"rules": {
"ban": [true, ["alert"]],
"no-arg": true,
"no-conditional-assignment": true,
"no-console": false,
"no-constant-condition": true,
"no-control-regex": true,
"no-debugger": true,
"no-duplicate-case": true,
"no-empty": true,
"no-empty-character-class": true,
"no-eval": true,
"no-ex-assign": true,
"no-extra-boolean-cast": true,
"no-extra-semi": true,
"no-switch-case-fall-through": true,
"no-inner-declarations": [true, "functions"],
"no-invalid-regexp": true,
"no-invalid-this": false,
"no-irregular-whitespace": true,
"ter-no-irregular-whitespace": true,
"label-position": true,
"indent": [true, "spaces", 2],
"linebreak-style": [true, "unix"],
"no-multi-spaces": true,
"no-consecutive-blank-lines": [true, 2],
"no-unused-expression": true,
"no-construct": true,
"no-duplicate-variable": true,
"no-regex-spaces": true,
"no-shadowed-variable": true,
"ter-no-sparse-arrays": true,
"no-trailing-whitespace": true,
"no-string-throw": true,
"no-unexpected-multiline": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"no-magic-numbers": false,
"array-bracket-spacing": [true, "never"],
"ter-arrow-body-style": false,
"ter-arrow-parens": [true, "as-needed"],
"ter-arrow-spacing": true,
"block-spacing": true,
"brace-style": [true, "1tbs", {"allowSingleLine": true}],
"variable-name": false,
"trailing-comma": [true, {"multiline": "never", "singleline": "never"}],
"cyclomatic-complexity": [false, 11],
"curly": [true, "all"],
"switch-default": false,
"eofline": true,
"triple-equals": true,
"forin": false,
"handle-callback-err": true,
"ter-max-len": [true, 120],
"new-parens": true,
"object-curly-spacing": [true, "never"],
"object-literal-shorthand": false,
"one-variable-per-declaration": [true, "ignore-for-loop"],
"ter-prefer-arrow-callback": false,
"prefer-const": true,
"object-literal-key-quotes": false,
"quotemark": [true, "single"],
"radix": true,
"semicolon": [true, "never"],
"space-in-parens": [true, "never"],
"comment-format": [true, "check-space"],
"use-isnan": true,
"valid-typeof": true
}
}

File diff suppressed because it is too large Load Diff