mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-12-01 01:25:48 +00:00
Refactor and use TypeScript:
- Migrate to TypeScript and TSLint - Vendor ripple-address-codec (move dependency into this project) - Add support for Travis CI (.travis.yml) - Use "dist/*" for distribution files - Bump version to 1.0.0-beta.0 - Add yarn.lock
This commit is contained in:
@@ -1,20 +1,22 @@
|
||||
'use strict' // eslint-disable-line strict
|
||||
import * as assert from 'assert'
|
||||
import * as brorand from 'brorand'
|
||||
import * as hashjs from 'hash.js'
|
||||
import * as elliptic from 'elliptic'
|
||||
|
||||
import * as addressCodec from './ripple-address-codec'
|
||||
import {derivePrivateKey, accountPublicFromPublicGenerator} from './secp256k1'
|
||||
import * as utils from './utils'
|
||||
|
||||
const assert = require('assert')
|
||||
const brorand = require('brorand')
|
||||
const hashjs = require('hash.js')
|
||||
const elliptic = require('elliptic')
|
||||
const Ed25519 = elliptic.eddsa('ed25519')
|
||||
const Secp256k1 = elliptic.ec('secp256k1')
|
||||
const addressCodec = require('ripple-address-codec')
|
||||
const derivePrivateKey = require('./secp256k1').derivePrivateKey
|
||||
const accountPublicFromPublicGenerator = require('./secp256k1')
|
||||
.accountPublicFromPublicGenerator
|
||||
const utils = require('./utils')
|
||||
|
||||
const hexToBytes = utils.hexToBytes
|
||||
const bytesToHex = utils.bytesToHex
|
||||
|
||||
function generateSeed(options = {}) {
|
||||
function generateSeed(options: {
|
||||
entropy?: Uint8Array,
|
||||
algorithm?: 'ed25519' | 'secp256k1'
|
||||
} = {}) {
|
||||
assert(!options.entropy || options.entropy.length >= 16, 'entropy too short')
|
||||
const entropy = options.entropy ? options.entropy.slice(0, 16) : brorand(16)
|
||||
const type = options.algorithm === 'ed25519' ? 'ed25519' : 'secp256k1'
|
||||
@@ -99,7 +101,7 @@ function verify(messageHex, signature, publicKey) {
|
||||
return select(algorithm).verify(hexToBytes(messageHex), signature, publicKey)
|
||||
}
|
||||
|
||||
function deriveAddressFromBytes(publicKeyBytes) {
|
||||
function deriveAddressFromBytes(publicKeyBytes: Buffer) {
|
||||
return addressCodec.encodeAccountID(
|
||||
utils.computePublicKeyHash(publicKeyBytes))
|
||||
}
|
||||
@@ -114,11 +116,14 @@ function deriveNodeAddress(publicKey) {
|
||||
return deriveAddressFromBytes(accountPublicBytes)
|
||||
}
|
||||
|
||||
const decodeSeed = addressCodec.decodeSeed
|
||||
|
||||
module.exports = {
|
||||
generateSeed,
|
||||
deriveKeypair,
|
||||
sign,
|
||||
verify,
|
||||
deriveAddress,
|
||||
deriveNodeAddress
|
||||
deriveNodeAddress,
|
||||
decodeSeed
|
||||
}
|
||||
220
packages/ripple-keypairs/src/ripple-address-codec/index.ts
Normal file
220
packages/ripple-keypairs/src/ripple-address-codec/index.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 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: number[], opts: {
|
||||
versions: number[],
|
||||
expectedLength: number
|
||||
}) {
|
||||
const versions = opts.versions
|
||||
return this.encodeVersioned(bytes, versions, opts.expectedLength)
|
||||
}
|
||||
|
||||
encodeVersioned(bytes: number[], 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?: string[]
|
||||
} = {}) {
|
||||
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 NODE_PRIVATE = 32
|
||||
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 an array (or Buffer?) of size 16
|
||||
// type is 'ed25519' or 'secp256k1'
|
||||
export function encodeSeed(entropy: number[], 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?: string[],
|
||||
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: number[]): 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: number[]): 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
|
||||
}
|
||||
59
packages/ripple-keypairs/src/ripple-address-codec/utils.ts
Normal file
59
packages/ripple-keypairs/src/ripple-address-codec/utils.ts
Normal 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], Buffer.from([4,5]), new Uint8Array([6, 7]));
|
||||
* [1,2,3,4,5,6,7]
|
||||
*
|
||||
* @return {Array} - concatenated arguments
|
||||
*/
|
||||
function concatArgs(): number[] {
|
||||
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
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
'use strict' // eslint-disable-line strict
|
||||
import * as elliptic from 'elliptic'
|
||||
import Sha512 from './sha512'
|
||||
|
||||
const elliptic = require('elliptic')
|
||||
const secp256k1 = elliptic.ec('secp256k1')
|
||||
const Sha512 = require('./sha512')
|
||||
|
||||
function deriveScalar(bytes, discrim) {
|
||||
// TODO: type of `discrim`?
|
||||
function deriveScalar(bytes, discrim?: any) {
|
||||
const order = secp256k1.curve.n
|
||||
for (let i = 0; i <= 0xFFFFFFFF; i++) {
|
||||
// We hash the bytes to find a 256 bit number, looping until we are sure it
|
||||
@@ -32,7 +32,10 @@ function deriveScalar(bytes, discrim) {
|
||||
* @return {bn.js} - 256 bit scalar value
|
||||
*
|
||||
*/
|
||||
function derivePrivateKey(seed, opts = {}) {
|
||||
export function derivePrivateKey(seed, opts: {
|
||||
validator?: boolean,
|
||||
accountIndex?: number
|
||||
} = {}) {
|
||||
const root = opts.validator
|
||||
const order = secp256k1.curve.n
|
||||
|
||||
@@ -51,15 +54,10 @@ function derivePrivateKey(seed, opts = {}) {
|
||||
.add(privateGen).mod(order)
|
||||
}
|
||||
|
||||
function accountPublicFromPublicGenerator(publicGenBytes) {
|
||||
export function accountPublicFromPublicGenerator(publicGenBytes) {
|
||||
const rootPubPoint = secp256k1.curve.decodePoint(publicGenBytes)
|
||||
const scalar = deriveScalar(publicGenBytes, 0)
|
||||
const point = secp256k1.g.mul(scalar)
|
||||
const offset = rootPubPoint.add(point)
|
||||
return offset.encodeCompressed()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
derivePrivateKey,
|
||||
accountPublicFromPublicGenerator
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
'use strict' // eslint-disable-line strict
|
||||
import * as hashjs from 'hash.js'
|
||||
import * as BigNum from 'bn.js'
|
||||
|
||||
const hashjs = require('hash.js')
|
||||
const BigNum = require('bn.js')
|
||||
export default class Sha512 {
|
||||
// TODO: type of `hash`?
|
||||
hash: any
|
||||
|
||||
module.exports = class Sha512 {
|
||||
constructor() {
|
||||
this.hash = hashjs.sha512()
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
'use strict' // eslint-disable-line strict
|
||||
|
||||
const assert = require('assert')
|
||||
const hashjs = require('hash.js')
|
||||
const BN = require('bn.js')
|
||||
import * as assert from 'assert'
|
||||
import * as hashjs from 'hash.js'
|
||||
import * as BN from 'bn.js'
|
||||
|
||||
function bytesToHex(a) {
|
||||
return a.map(function(byteValue) {
|
||||
@@ -16,7 +14,7 @@ function hexToBytes(a) {
|
||||
return (new BN(a, 16)).toArray(null, a.length / 2)
|
||||
}
|
||||
|
||||
function computePublicKeyHash(publicKeyBytes) {
|
||||
function computePublicKeyHash(publicKeyBytes: Buffer): number[] {
|
||||
const hash256 = hashjs.sha256().update(publicKeyBytes).digest()
|
||||
const hash160 = hashjs.ripemd160().update(hash256).digest()
|
||||
return hash160
|
||||
@@ -26,7 +24,7 @@ function seedFromPhrase(phrase) {
|
||||
return hashjs.sha512().update(phrase).digest().slice(0, 16)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
bytesToHex,
|
||||
hexToBytes,
|
||||
computePublicKeyHash,
|
||||
Reference in New Issue
Block a user