bip39toalgo.js

const bip39words = require('./bip39-en').words
const utils = require('./utils')
const hmacSHA512 = require('crypto-js/hmac-sha512')
const hmacSHA256 = require('crypto-js/hmac-sha256')
const PBKDF2 = require('crypto-js/pbkdf2')
const cp = require('crypto-js');
const EC = require('elliptic').ec;
const EdDSA = require('elliptic').eddsa;
const ecEd25519 = new EdDSA('ed25519');
const ecSECP256 = new EC('secp256k1');
const ecCurve25519 = new EC('ed25519');

const BIP32KEY_HARDEN = 0x80000000
const ed25519_n = 2n**252n + 27742317777372353535851937790883648493n
const _ = undefined

const hexilify   = cp.enc.Hex.stringify
const unhexilify = cp.enc.Hex.parse

const _hmac512  = (message, secret) => hmacSHA512(message, secret)
const _hmac256  = (message, secret) => hmacSHA256(message, secret)
const _getBit   = (character, pattern) => (character &  pattern) >>> 0
const _setBit   = (character, pattern) => (character |  pattern) >>> 0
const _clearBit = (character, pattern) => (character & ~pattern) >>> 0

// In JS, to do bitwise operations with unsigned ints, follow these rules:
// 1. Always end bitwise operations with >>> 0 so the result gets interpreted
//    as unsigned.
// 2. Don't use >>. If the left-most bit is 1 it will try to preseve the sign and 
//    thus will introduce 1's to the left. Always use >>>.
// 3. Only if the last op is >>>, >>> 0 is not necessary.
// Source: https://stackoverflow.com/questions/6798111/bitwise-operations-on-32-bit-unsigned-ints
const _OR  = (x,y) => (x | y) >>> 0
const _AND = (x,y) => (x & y) >>> 0
const _XOR = (x,y) => (x ^ y) >>> 0

// Source: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
const RED     = s => `\x1b[40m\x1b[31m${s}\x1b[0m`  //black background, red text
const YELLOW  = s => `\x1b[40m\x1b[93m${s}\x1b[0m`  //black background, yellow text
const GREEN   = s => `\x1b[40m\x1b[92m${s}\x1b[0m`  //black background, green text
const GREENBG = s => `\x1b[102m\x1b[30m${s}\x1b[0m` //green background, black text

const _DBUG = false
const TRACE = (k,v, debug=_DBUG) => {
    if(debug) console.log(k.padEnd(12),v)
}
const ENTER = (g   , debug=_DBUG) => { if(debug) console.group(YELLOW('ENTER ' + g)) }
const LEAVE = (g='', debug=_DBUG) => { if(debug) {console.groupEnd(); console.log(YELLOW('LEAVE ' + g))} }

/** @namespace bip39toalgo */

/**
 * @typedef  {Object}   bip39toalgo.WordArray
 * @memberof bip39toalgo
 * @property {number[]} words Bytes array as signed integers
 * @property {number}   sigBytes
 */
 
 /**
 * Stores order of elliptic curve and 
 * {@link https://github.com/satoshilabs/slips/blob/master/slip-0010.md|SLIP10}
 * modifier for master key generation.
 * @typedef  {Object} bip39toalgo.CurveParams
 * @memberof bip39toalgo
 * @property {string} name      Name of elliptic curve
 * @property {string} modifier  Key to use in HMAC-SHA512 as per SLIP10
 * @property {BigInt} order     Order of the elliptic curve
 */

/** 
 * @typedef  {Object}   bip39toalgo.AlgoData
 * @memberof bip39toalgo
 * @property {Object}   algo
 * @property {string}   algo.key        Algorand private key in hexadecimal
 * @property {address}  algo.address    Algorand public wallet address
 * @property {string[]} algo.words      Algorand mnemonic (25 words)
 * @property {string}   algo.pub        Algorand public key in hexadecimal
 * @property {string=}  algo.chk1       Public key cheksum
 * @property {string=}  algo.chk2       Mnemonic cheksum
 */

/**
 * @typedef  {Object}              bip39toalgo.DerivationNode
 * @memberof bip39toalgo
 * @property {(string|bip39toalgo.WordArray)}  kL    Leftmost 32 bytes of private key
 * @property {(string|bip39toalgo.WordArray)}  kR    Rightmost 32 bytes of private Key
 * @property {(string|bip39toalgo.WordArray)=} A     32 bytes public key (y coordinatte only)
 * @property {(string|bip39toalgo.WordArray)=} c     32 bytes chain code
 * @property {(string|bip39toalgo.WordArray)=} P     32 bytes public key
 * @property {bip39toalgo.AlgoData=}           algo
 */

/**
 * Algorand secret mnemonic (25 BIP39 words)
 * @typedef {string[]} bip39toalgo.AlgoSecretWords
 * @memberof bip39toalgo
 */

/**
 * @typedef  {Object}           bip39toalgo.AlgoAddressData
 * @memberof bip39toalgo
 * @property {string}           key
 * @property {string}           pub
 * @property {string}           address
 * @property {string=}          chk
 * @property {bip39toalgo.AlgoSecretWords=} words       Algorand secret words
 */

 /**
 * @typedef  {Object}           bip39toalgo.AlgoMnemonicData
 * @memberof bip39toalgo
 * @property {bip39toalgo.AlgoSecretWords}  words       Algorand secret words  
 * @property {string}           chk         Mnemonic checksum
 */

 /**
 * @typedef  {Object}           bip39toalgo.AlgoParsedMnemonicData
 * @memberof bip39toalgo
 * @property {string}           mnemonic    Parsed Algorand mnemonic
 * @property {string}           original    Original mnemonic normalized (NFKD)
 * @property {bip39toalgo.AlgoSecretWords}  words       Algorand secret words
 * @property {string}           key         Private key in hexadecimal
 * @property {string}           checksum    Mnemonic checksum
 * @property {boolean}          valid       Mnemonic validity
 */

  /**
 * @typedef  {Object}    bip39toalgo.Bip39ParsedMnemonicData
 * @memberof bip39toalgo
 * @property {string}    mnemonic    Parsed Algorand mnemonic
 * @property {string}    original    Original mnemonic normalized (NFKD)
 * @property {string[]}  words       Algorand secret words
 * @property {string}    checkbits   Checksum bits
 * @property {boolean}   valid       Mnemonic validity
 */

/**
 * Returns {@link bip39toalgo.DerivationNode} from arguments
 * @memberof bip39toalgo
 * @param {(string|bip39toalgo.WordArray)} kL   Leftmost 32 bytes of private key
 * @param {(string|bip39toalgo.WordArray)} kR   Rightmost 32 bytes of private Key
 * @param {{A: (string|bip39toalgo.WordArray), 
 *  c: (string|bip39toalgo.WordArray), 
 *  p: (string|bip39toalgo.WordArray)}} args
 * @returns {bip39toalgo.DerivationNode} Derivation node
 */
const _NODE = (kL,kR, ...args) => { 
    [ A, c, p ] = args
    o = { kL, kR, A, c, p }
    var dumps = () => kL.toString()
    return o
}
/** Assertion utility function
 * @returns {boolean}
 */
function _assert(x, y, op='eq'){
    // console.log(x, op, y)
    exp = false
    exp ^= op === 'eq' & x === y
    exp ^= op === 'gt' & x >   y
    exp ^= op === 'ge' & x >=  y
    exp ^= op === 'lt' & x <   y
    exp ^= op === 'le' & x <=  y
    if(exp) return true
    else throw EvalError(RED(`\n${x}\nNOT ${op}\n${y}`))
}
/**
 * Convert integers to BIP39 words
 * @memberof bip39toalgo
 * @param {number[]} nums 11-bit unsigned integers
 * @returns {string[]} List of BIP39 words
 */
const numsToWords = nums => nums.reduce((p,c) => [...p, bip39words[c]],[])
/**
 * Convert {@link https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed|BIP39} mnemonic to seed
 * @memberof bip39toalgo
 * @param {string} mnemonic    Mnemonic (12-24 words delimited by single space)
 * @param {string} passphrase  Passphrase as suffix for the salt
 * @param {string=} prefix     Modifier as prefix for the salt
 * @returns {bip39toalgo.WordArray} Seed
 */
function bip39seed(mnemonic, passphrase='',prefix='mnemonic'){
    return new Promise(function(resolve,reject){
        seed = cp.PBKDF2(mnemonic.normalize('NFKD'), prefix+passphrase,{
            hasher: cp.algo.SHA512,
            keySize: 512 / 32,
            iterations: 2048
        })
        if (seed.length === 0) reject('Error: empty seed')
        TRACE('bip39seed',seed.toString())
        resolve(seed)
    })
}
/**
 * Get elliptic curve parameters
 * @memberof bip39toalgo
 * @param {string} curveName Name of the elliptic curve
 * @returns {bip39toalgo.CurveParams} Curve parameters
 */
function curveInfo(curveName){
    curves = {
    secp256k1: {
                name:'secp256k1',
                modifier: 'Bitcoin seed',
                order: BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141')
            },
    nist256p1: {
                name:'nist256p1',
                modifier: 'Nist256p1 seed',
                order: BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551')
            },
    ed25519: {
                name:'ed25519',
                modifier: 'ed25519 seed',
            }
    }
    return curves[curveName]

}
/**
 * Derive root key (master node) using SLIP10 specs or
 * implementing paper from D. Khovratovich and J. Law
 * "BIP32-Ed25519: Hierarchical Deterministic Keys over a Non-linear Keyspace"
 * @memberof bip39toalgo
 * @param {bip39toalgo.WordArray}   seed Entropy to derive root key
 * @param {bip39toalgo.CurveParams} curve Curve parameters
 * @param {string}      [method='slip10'] Derivation method (slip10|kholaw)
 * @returns {Promise<bip39toalgo.DerivationNode>} Promise with derivation node
 */
function rootKey(seed, curve, method='slip10'){
    return new Promise((res,error)=>{
        ENTER('ROOT KEY')
        if(method==='slip10'){
            isAlive = true
            while(isAlive){
                h = hmacSHA512(seed,curve.modifier).toString()
                kL = unhexilify(h.substr(0,64))
                kR = unhexilify(h.substr(64))
                if(curve.name == 'ed25519') isAlive=false
                a = BigInt('0x'+kL)
                if(a<curve.order && a != 0) isAlive=false
                seed = unhexilify(h)
            TRACE('kL',kL.toString())
            TRACE('kR',kR.toString())
            LEAVE()
            res(_NODE(kL,kR))
            }
        } else if(method==='kholaw'){
            c = _hmac256(unhexilify('01'+seed),curve.modifier)
            I = _hmac512(seed, curve.modifier).toString()
            kL = unhexilify(I.substr(0,64))
            kR = unhexilify(I.substr(64))
            kLb = utils.hexToBytes(kL.toString())
            while (_getBit(kLb[31], 0b00100000) !=0){
                seed = unhexilify(I)
                I = _hmac512(seed, curve.modifier).toString()
                kL = unhexilify(I.substr(0,64))
                kR = unhexilify(I.substr(64))
                kLb = utils.hexToBytes(kL.toString())
            }

            kLb[0]  = _clearBit( kLb[0], 0b00000111)
            kLb[31] = _clearBit(kLb[31], 0b10000000)
            kLb[31] =   _setBit(kLb[31], 0b01000000)

            kL = unhexilify(utils.bytesToHex(kLb))
            kLr = utils.bytesToHex(kLb.reverse())

            pub  = ecCurve25519.keyFromPrivate(kLr).getPublic()
            x = pub.getX().toString('hex')
            y = pub.getY().toString('hex')
            A = encodeXY(x,y)

            TRACE('scalar', BigInt('0x'+kLr).toString(10))
            TRACE('x',x)
            TRACE('y',y)

            TRACE('kL',kL.toString())
            TRACE('kR',kR.toString())
            TRACE('A',A)
            TRACE('c',c.toString())
            LEAVE()

            res(_NODE(kL,kR,A,c))
        }
    })
}
/**
 * Computes public key for given curve
 * @memberof bip39toalgo
 * @param {(string|bip39toalgo.WordArray)} key Private key
 * @param {bip39toalgo.CurveParams} curve Curve parameters
 * @returns {string} Public key in hexadecimal
 */
function getPublicKey(key,curve){
    if (curve.name == 'ed25519'){
        k = '00' + utils.bytesToHex(ecEd25519.keyFromSecret(key.toString()).getPublic())
    }
    else if(curve.name == 'secp256k1'){
        pub  = ecSECP256.keyFromPrivate(key.toString()).getPublic()
        x    = pub.getX().toString('hex') // BN -> hex
        y    = pub.getY().toString('hex') // BN -> hex
        padx = x.padStart(64,'0')
        pady = y.padStart(64,'0')
        if (BigInt('0x' + y) & 1n) {
            k = '03' + padx
        } else{
            k = '02' + padx
        }
}
    return k
}
/**
 * Derives child key from parent key data using SLIP10 specs
 * @memberof bip39toalgo
 * @param {(string|bip39toalgo.WordArray)} parentKey    Parent node private key
 * @param {bip39toalgo.WordArray} parentChaincode       Parent node chain code
 * @param {number} i                        Current path index
 * @param {bip39toalgo.CurveParams} curve               Curve params
 * @returns {Promise<bip39toalgo.DerivationNode>}       Child node
 */
function deriveChild(parentKey, parentChaincode, i, curve){
    return new Promise((res,error)=>{
        ENTER('DERIVE CHILD SLIP10')
        data = ''
        if(_AND(i, BIP32KEY_HARDEN)){
            data = '00' + parentKey.toString()
        } else {
            data = getPublicKey(parentKey, curve)
        }
        data += i.toString(16).padStart(8,0) //padded 4 bytes

        while(true){
            h = hmacSHA512(unhexilify(data), parentChaincode).toString()
            kL = unhexilify(h.substr(0,64))
            kR = unhexilify(h.substr(64))
            if(curve.name == 'ed25519') break
            a = BigInt('0x'+kL)
            key = (a + BigInt('0x' + parentKey)) % curve.order

            if(a<curve.order &&  key!= 0){
                kL = unhexilify(key.toString(16).padStart(64,0))
                break
            }
            data = '01' + hexilify(kR) +  i.toString(16).padStart(8,0)
        }

        pub = getPublicKey(kL,curve)

        o = _NODE(kL,kR,_,_,pub)

        TRACE('private',o.kL.toString().padStart(64,0))
        TRACE('chain',o.kR.toString().padStart(64,0))
        TRACE('public',o.p)
        LEAVE()
        res(o)
    })
}
/**
 * Encodes elliptic curve X-coordinate into Y-coordinate
 * @memberof bip39toalgo
 * @param {string} x X-coordinate bytes in hexadecimal
 * @param {string} y Y-coordinate bytes in hexadecimal
 */
function encodeXY(x,y){
    xb = utils.hexToBytes(x)
    yb = utils.hexToBytes(y)
    if(_AND(xb[31],1)){
        yb[0] = (yb[0] | 0x80) >>> 0
    }
    return utils.bytesToHex(yb.reverse())
}
/**
 * Derive child key by implementing paper from D. Khovratovich and J. Law
 * "BIP32-Ed25519: Hierarchical Deterministic Keys over a Non-linear Keyspace"
 * @memberof bip39toalgo
 * @param {bip39toalgo.DerivationNode} node         Parent node
 * @param {number} i                    Current path index
 * @returns {Promise<bip39toalgo.DerivationNode>}   Child node
 */
function deriveChildKhoLaw(node, i){
    ENTER('DERIVE CHILD KHO-LAW')
    return new Promise((res,error)=>{
        kLP = node.kL
        kRP = node.kR
        AP = node.A
        cP = node.c

        ib = utils.reverseHex(i.toString(16).padStart(4*2,'0'))
        
        // TRACE('\nDERIVE CHILD KEY:','')
        TRACE('kLP',hexilify(kLP))
        TRACE('kRP',hexilify(kRP))
        TRACE('AP',AP)
        TRACE('cP',hexilify(cP))
        TRACE('i',i)
        TRACE('ib',ib)

        if(i < 2**31){
            // regular child
            Zi = '02' + AP + ib
            ci = '03' + AP + ib
            Z = _hmac512(unhexilify(Zi), cP).toString()
            c = _hmac512(unhexilify(ci), cP).toString().substr(-32*2)
            TRACE('Zi reg',Zi)
            TRACE('ci reg',ci)
        } else{
            // hardened child
            Zi = '00' + hexilify(kLP) + hexilify(kRP) + ib
            ci = '01' + hexilify(kLP) + hexilify(kRP) + ib
            Z = _hmac512(unhexilify(Zi), cP).toString().toString()
            c = _hmac512(unhexilify(ci), cP).toString().substr(-32*2)
            TRACE('Zi hard',Zi)
            TRACE('ci hard',ci)
        }
        TRACE('Z',Z)
        TRACE('c',c)

        ZL = unhexilify(Z.substr(0,28*2))
        ZR = unhexilify(Z.substr(32*2))

        // compute KRi
        kLn = BigInt('0x'+utils.reverseHex(hexilify(ZL))) * 8n 
            + BigInt('0x'+utils.reverseHex(hexilify(kLP)))
        
        TRACE('ZL',ZL.toString())
        TRACE('ZR',ZR.toString())
        TRACE('kLn',kLn.toString(16))

        if(kLn % ed25519_n == 0n){
            TRACE('kLn is 0','kLn % ed25519')
            res()
        }

        // compute KLi
        kRn = (
            BigInt('0x'+utils.reverseHex(hexilify(ZR)))
          + BigInt('0x'+utils.reverseHex(hexilify(kRP)))
             ) % 2n**256n

        TRACE('kRn',kRn.toString(16))

        kL = utils.reverseHex(kLn.toString(16))
        kR = utils.reverseHex(kRn.toString(16))
        TRACE('kL',kL.toString(16))
        TRACE('kR',kR.toString(16))

        pub  = ecCurve25519.keyFromPrivate(utils.reverseHex(kL)).getPublic()

        x = pub.getX().toString('hex')
        y = pub.getY().toString('hex')
        A = encodeXY(x,y)

        TRACE('scalar', BigInt('0x'+utils.reverseHex(kL)).toString(10))
        TRACE('x',x)
        TRACE('y',y)
        TRACE('A',A)
        LEAVE()

        o =_NODE(unhexilify(kL),unhexilify(kR),A,unhexilify(c))
        res(o)
    })
}

 /**
  * Computes Algorand address and mnemonic from {@link bip39toalgo.DerivationNode}
  * @memberof bip39toalgo
  * @param {bip39toalgo.DerivationNode} node
  * @returns {Promise<bip39toalgo.DerivationNode>} Derivation node with Algorand's secret 
  */
function algoSecret(node){
    ENTER('ALGORAND SECRET')
    return new Promise((res,error)=>{
        var { key, pub, address, chk } = algoAddress(node.kL)
        chk1 = chk
        var { words, chk } = algoMnemonic(key)
        chk2 = chk
        TRACE('key',key)
        TRACE('pub',pub)
        TRACE('pub_chk',chk1)
        TRACE('addr',address)
        TRACE('mnemo_chk',chk2)
        TRACE('words',words)
        LEAVE()
        node.algo = { key,address,words,pub,chk1,chk2 }
        res(node)
    })
}

/**
 * Derives Algorand's public key from private key
 * @memberof bip39toalgo
 * @param {(string|bip39toalgo.WordArray)} key
 * @returns {bip39toalgo.AlgoAddressData} Algorand's address data
 */
function algoAddress(key){
    key = key.toString().padStart(64,'0')
    pub = utils.bytesToHex(ecEd25519.keyFromSecret(key).getPublic())
    chk = hexilify(cp.SHA512t256(unhexilify(pub))).substr(0,64).substr(-8)
    address = utils.hex2b32(pub+chk).replace(/=/g,'')
    return { key, pub, address, chk }
}
/**
 * Translates Algorand private key to mnemonic words
 * @memberof bip39toalgo
 * @param {string} key Private key in hexadecimal
 * @returns {bip39toalgo.AlgoMnemonicData} Algorand's mnemonic data
 */
function algoMnemonic(key){
    nums = utils.bytes2b11(utils.hexToBytes(key))
    words = numsToWords(nums)
    chk = cp.SHA512t256(unhexilify(key)).toString().substr(0,2*2)
    chkN = utils.bytes2b11(utils.hexToBytes(chk))
    chkW = numsToWords(chkN)[0]
    words.push(chkW)
    return { words, chk }
}
/**
 * Generates random Algorand address
 * @memberof bip39toalgo
 * @returns {bip39toalgo.AlgoAddressData} Algorand's address data
 */
const randomAlgoAddress = () => utils.randomHex(32).then(ent => algoAddress(ent))

/**
 * Translates Algorand mnemonic to private key
 * @memberof bip39toalgo
 * @param {string} mnemonic 
 * @returns {bip39toalgo.AlgoParsedMnemonicData} Algorand's parsed mnemonic data
 */
function algoKeyFromMnemonic(mnemonic){
    mnemonic = mnemonic.trim().toLowerCase().normalize('NFKD').split(' ')
    if(mnemonic.length !== 25) throw new Error('Invalid mnemonic length: expected 25 words')
    words = mnemonic.map(w => bip39words.find(bw => bw.substr(0,4)==w.substr(0,4)))
    nums = words.map(w => bip39words.findIndex(bw => bw==w))
    if(nums.length !== 25) throw new Error('Invalid mnemonic: one or more words not valid')
    // last word is the checksum:
    csN1 = nums.slice(-1)[0]
    cs1 = csN1.toString(16)
    // convert 11-bit numbers (little endian) to bits:
    bits = nums.slice(0,24).map((e,i) => e.toString(2).padStart(11,'0')).reverse().join('')
    key = utils.reverseHex(utils.bits2hex(bits)).substr(0,64)
    // compute the checksum to verify mnemonic:
    cs2 = cp.SHA512t256(unhexilify(key)).toString().substr(0,2*2)
    csN2 = utils.bytes2b11(utils.hexToBytes(cs2))[0]
    isValid = csN1 === csN2
    parsed = { 
        mnemonic:words.join(' '),
        original: mnemonic.join(' '),
        words:words,
        key:key,
        checksum:cs1, 
        valid:isValid,
    }
    return parsed
}
/**
 * Derives Algorand public key and address
 * @memberof bip39toalgo
 * @param {string} mnemonic Algorand mnemonic
 * @returns {bip39toalgo.AlgoAddressData} Algorand's address data
 */
function algoAddressFromMnemonic(mnemonic){
    var { key, words, valid } = algoKeyFromMnemonic(mnemonic)
    if(!valid) throw new Error('Invalid mnemonic checksum')
    var { pub, address } = algoAddress(key)
    return { key, pub, address, words }
}
/**
 * Generates N random addresses and counts occurrences of last character
 * @memberof bip39toalgo
 * @param {number} [n=1000] Number of addresses to generate
 * @returns {void} Nothing
 */
function countAddressEnding(n=1000){
    let b32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.split('')
    let b32map = Object.fromEntries(new Map(b32.map(e => [e,0])))
    let endChars = utils.range(n).map((e,i,a) => randomAlgoAddress().then(algo => {
        if((i+1)%1000==0) console.log(i, algo.address)
        return algo.address.substr(-1)
    }))
    Promise.all(endChars).then(chars => {
        for (let i = chars.length - 1; i >= 0; i--) {
            let c = chars[i]
            b32map[c]++
        }
        console.log(b32map)
    })
}
/**
 * Computes Algorand address and mnemonic from private key
 * @memberof bip39toalgo
 * @param {string} key Private key in hexadecimal
 * @returns {bip39toalgo.AlgoAddressData} Algorand's address data
 */
function algoWords(key){
    return new Promise((res,error)=>{
        var { pub, address } = algoAddress(key)
        var { words } = algoMnemonic(key)
        algo = { key, pub, address, words }
        res(algo)
    })
}
/**
 * Derives Algorand's secret from BIP39 seed and using given method and path
 * @memberof bip39toalgo
 * @param {bip39toalgo.WordArray}   seed    BIP39 seed bytes
 * @param {string}      method  Derivation method
 * @param {string=}     path    Derivation path
 * @returns {Promise<bip39toalgo.DerivationNode>} Derivation node with Algorand's secret
 */
function deriveBip39Seed(seed, method, path="m/44'/283'/0'/0/0"){
    TRACE('method',method)
    TRACE('path',path)

    if(method==='bip39-seed'){
        let o = _NODE()
        o.seed = seed
        o.bip39seed = seed.toString()
        o.kL = unhexilify(seed.toString().substr(0,32*2))
        TRACE('kL',o.kL.toString().padStart(64,0))
        return algoSecret(o)
    }

    curve = curveInfo(method.split('-')[1])
    method = method.split('-')[0]

    return rootKey(seed,curve,method)
    .then(root => {
        TRACE('m_private',root.kL.toString())
        TRACE('m_chain',root.kR.toString())

        path = path.split('/')
        // path.shift(0)
        if(path.indexOf('m') === 0) [ignore, ...path] = path

        return path.reduce((p,c,i,a) => {
            return p.then(o=>{
                idx = parseInt(c)
                if (c.substr(-1) === "'") idx = _OR(idx, BIP32KEY_HARDEN)
                if (curve.name === 'ed25519' && method == 'slip10') idx = _OR(idx, BIP32KEY_HARDEN)
                currPath = a.slice(0,i+1).join('/')
                ENTER(currPath)
                TRACE('parent key',o.kL.toString())
                if(method=='slip10') return deriveChild(o.kL, o.kR, idx, curve).then(o=>{ LEAVE(''); return o })
                if(method=='kholaw') return deriveChildKhoLaw(o, idx).then(o=>{ LEAVE(''); return o })
            })
        }, Promise.resolve(root))
        .then(o=>{
            o.seed = seed
            o.bip39seed = seed.toString()
            return o
        })
    })
    .then(node => algoSecret(node))
}
/**
 * Derives Algorand's secret from BIP39 mnemonic and using given method and path
 * @memberof bip39toalgo
 * @param   {string}    mnemonic    BIP39 mnemonic
 * @param   {string}    method      Derivation method
 * @param   {string=}   path        Derivation path
 * @param   {string=}   passphrase  BIP39 mnemonic passphrase
 * @returns {Promise<bip39toalgo.DerivationNode>} Derivation node with Algorand secret
 * @example
 * // Returns:
 * // 7b6ec191cb3b77f6593cefaddf0489af47bb65e0f4480391bcedd00caa822d11
 * // NMRBZNN2RXUNVLVVPVD53GJV6A2A55QWJXMD2KG42N7NQZB67WXYFGONVA
 * //  1. sorry       6. laugh      11. setup      16. employ     21. favorite   
 * //  2. aisle       7. tissue     12. kit        17. call       22. gaze       
 * //  3. similar     8. upset      13. isolate    18. venture    23. maximum    
 * //  4. royal       9. volcano    14. bonus      19. item       24. abandon    
 * //  5. unveil     10. beach      15. poem       20. snack      25. leave
 * mnemonic = 'all all all all all all all all all all all all all all all all all all all all all all all feel'
 * deriveMnemonic(mnemonic,"slip10-ed25519", "m/44'/283'/0'/0/0")
 * .then(node => {
 *     console.log(node.algo.key)
 *     console.log(node.algo.address)
 *     words = prettifyWordsTTB(node.algo.words)
 *     console.log(words)
 * })
 */
function deriveMnemonic(mnemonic, method, path, passphrase=''){
    return bip39seed(mnemonic,passphrase).then(seed => deriveBip39Seed(seed, method, path))
}
/**
 * Formats list of 25 words in a 5x5 grid, indexed Left-to-Right
 * @memberof bip39toalgo
 * @param {bip39toalgo.AlgoSecretWords} words - Algorand secret words
 * @returns {string} Formatted words list with line breaks
 */
function prettifyWordsLTR(words){
    prettyWords = []
    row = []
    words.map((w,i)=>{
            w = ((i+1).toString().padStart(2) + '. ' + w).padEnd(15)
            row.push(w)
            if((i+1)%5==0) {
                prettyWords.push(row.join(''))
                row = []
            }
        })
    return prettyWords.join('\n')
}
/**
 * Formats list of 25 words in a 5x5 grid, indexed Top-to-Bottom
 * @memberof bip39toalgo
 * @param {bip39toalgo.AlgoSecretWords} words - Algorand secret words
 * @returns {string} Formatted words list with line breaks
 */
function prettifyWordsTTB(words){
    prettyWords = words.map((w,i)=>{
        w = ((i+1).toString().padStart(2) + '. ' + w).padEnd(15)
        if(i>=20) w += '\n'
        return w
    }).map((w,i,a)=>{
        return a[5*(i%5) + Math.floor(i/5)]
    })
    return prettyWords.join('')
}
/**
 * Computes BIP39 checksum bits for given entropy
 * @memberof bip39toalgo
 * @param {string} ent Entropy bytes in hexadecimal
 * @param {number} cs  Checksum length in bits
 * @returns {string} Checksum bits
 */
function entCheckBits(ent, cs){
    chk = cp.SHA256(unhexilify(ent)).toString().substr(0,2) //get first byte
    return utils.hex2bits(chk).substr(0,cs).padStart(cs)
}
/**
 * Translates entropy into BIP39 mnemonic words
 * @memberof bip39toalgo
 * @param   {string}   ent 
 * @returns {string[]} BI39 words list
 */
function ent2bip39words(ent){
    cs = ent.length*8/2/32
    entChecked = utils.hex2bits(ent).substr(0,ent.length*8/2+cs)+entCheckBits(ent,cs)
    nums = utils.bits2uintN(11,entChecked)
    wlist = numsToWords(nums)
    return wlist
}
/**
 * Generates random BIP39 words
 * @memberof bip39toalgo
 * @param {number} size Entropy size in bytes (16|20|24|28|32)
 * @returns {string} Mnemonic words
 */
const randomWords = size => utils.randomHex(size).then(r => ent2bip39words(r)).then(w => w.join(' '))
/**
 * Find word in BIP39 wordlist
 * @memberof bip39toalgo
 * @param {string} word BIP39 word to search
 * @returns {(string|undefined)} Found word
 */
function findBip39Word(word){
    w = word.trim().toLowerCase().normalize('NFKD').substr(0,4)
    return bip39words.find(bw => bw.substr(0,4)==w)
}
/**
 * Parses BIP39 mnemonic and verifies validity
 * @memberof bip39toalgo
 * @param {string} mnemonic 
 * @returns {bip39toalgo.Bip39ParsedMnemonicData}
 */
function parseMnemonic(mnemonic){
    mnemonic = mnemonic.trim().toLowerCase().normalize('NFKD').split(' ')
    words = mnemonic.map(w => bip39words.find(bw => bw.substr(0,4)==w.substr(0,4)))
    nums = words.map(w => bip39words.findIndex(bw => bw==w))
    bits = utils.uintN2bits(11,nums)
    cs = bits.length % 32
    ent = utils.bits2hex(bits.substr(0,bits.length-cs))
    chkBits1 = bits.substr(-cs)
    chkBits2 = entCheckBits(ent, cs)
    isValid = chkBits1 === chkBits2
    parsed = { 
        mnemonic:words.join(' '),
        original: mnemonic.join(' '),
        words:words, 
        checkbits:chkBits1, 
        valid:isValid,
    }
    return parsed
}
/**
 * Generate dummy BIP39 mnemonic for testing
 * @memberof bip39toalgo
 * @param {string} [word='all'] Dummy BIP39 word to repeat 
 * @param {number} [size=24]    Number of words (12|15|18|21|24)
 * @example
 * // returns "dog dog dog dog dog dog dog dog dog dog dog dose"
 * console.log(testMnemonicWords('dog',12).join(' '))
 * @example
 * // returns "boy boy boy boy boy boy boy boy boy boy boy boy boy boy boss"
 * console.log(testMnemonicWords('boy',15).join(' '))
 * @example
 * // returns "bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar anxiety"
 * console.log(testMnemonicWords('bar',24).join(' '))
 */
function testMnemonicWords(word='all',size=24){
    dummyMnemonic = `${word.trim()} `.repeat(size).trim()
    mnemonic = dummyMnemonic.trim().toLowerCase().normalize('NFKD').split(' ')
    words = mnemonic.map(w => bip39words.find(bw => bw.substr(0,4)==w.substr(0,4)))
    nums = words.map(w => bip39words.findIndex(bw => bw==w))
    bits = utils.uintN2bits(11,nums)
    cs = bits.length % 32
    ent = utils.bits2hex(bits.substr(0,bits.length-cs))
    // chkBits = entCheckBits(ent, cs)
    return ent2bip39words(ent)
}
/**
 * Derive mnemonic for given test vector
 * @memberof bip39toalgo
 * @param {{ no: number, mnemonic: string, 
 *  method: string, path: string, key: string, 
 *  address: string }} testVector
 * @returns {void} Nothing
 */
function deriveMnemonicTest({ no, mnemonic, method, path, key, address }) {
    ENTER(`Test #${no}: ${method}`, true)
    return deriveMnemonic(mnemonic, method, path)
    .then(o=>{
        // console.log(o.algo)
        TRACE('test key', key, true)
        TRACE('test address', address, true)
        let { valid } = parseMnemonic(mnemonic)
        _assert(valid, true)
        _assert(o.algo.key, key)
        _assert(o.algo.address, address)
        console.log(prettifyWordsLTR(o.algo.words))
        TRACE(GREENBG('assertion'), GREENBG('OK'), true)
        return true
    })
    .then(done => LEAVE('', true))
}
/**
 * Run tests and log to console
 * @memberof bip39toalgo
 * @returns {void} Nothing
 */
function tests() {
    vectors = [
        { 
            no:         1,
            mnemonic:   'all all all all all all all all all all all all all all all all all all all all all all all feel',
            method:     wallets.ledger.method,
            path:       wallets.ledger.path,
            key:        '1075ab5e3fcedcb69eef77974b314cc0cbc163c01a0c354989dc70b8789a194f',
            address:    'NVGXFOROGBDBUW6CEQDX6V742PWFPLXUDKW6V7HOZHFD7GSQEB556GUZII'
        },
        { 
            no:         2,
            mnemonic:   'all all all all all all all all all all all all all all all all all all all all all all all feel',
            method:     wallets.coinomi.method,
            path:       wallets.coinomi.path,
            key:        '7b6ec191cb3b77f6593cefaddf0489af47bb65e0f4480391bcedd00caa822d11',
            address:    'NMRBZNN2RXUNVLVVPVD53GJV6A2A55QWJXMD2KG42N7NQZB67WXYFGONVA'
        },
        { 
            no:         3,
            mnemonic:   'all all all all all all all all all all all all',
            method:     wallets.exodus.method,
            path:       wallets.exodus.path,
            key:        '0c9b6a753e82afef190302853c14cdadc8d229cec3196ee464e41f0bc5c2519e',
            address:    'ZXLNDDUAYCYFXJI33HXUXLNVUTMQMSG6HRXV6JT2KNSU2SP4J7GUZG5BWU'
        },
        { 
            no:         4,
            mnemonic:   'all all all all all all all all all all all all',
            method:     wallets.atomic.method,
            path:       wallets.atomic.path,
            key:        'c76c4ac4f4e4a00d6b274d5c39c700bb4a7ddc04fbc6f78e85ca75007b5b495f',
            address:    'YQDDGDM3BKPQ5RAIYGCT7JX6DCIMVQHTHITSPJWKNLIPETB2JR6MPKC43A'
        },
        { 
            no:         5,
            mnemonic:   'bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar anxiety',
            method:     wallets.ledger.method,
            path:       wallets.ledger.path,
            key:        'c896059cbb23f5e29692ce23c5c56aeea6376ae63dfb513e03e42b75be51e646',
            address:    'KS4ACRBVNAKFAEKK5XWV5HV355FDPBRNG37VTJYU646WLAGWD26L6FSIRA'
        },
        { 
            no:         6,
            mnemonic:   'bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar bar anxiety',
            method:     wallets.trust.method,
            path:       wallets.trust.path,
            key:        '83fffaec238ae65b1ef4195d01d6c670348335f78ee6407e70c07cd356cd462e',
            address:    'DDVQJSNA7KMZAR3WTZXQHB53KKXHI7AGQOQSPLQL4Y5PTY7IMNTATQMTAE'
        },
        { 
            no:         7,
            mnemonic:   'dog dog dog dog dog dog dog dog dog dog dog dose',
            method:     wallets.exodus.method,
            path:       wallets.exodus.path,
            key:        '9bcbf75ea8b0997771c19e8440e3bce7675374bbe926f608cdbf671d42171966',
            address:    'QKYJ7CY3ZDJZ7GZE7FJ6S5WK5MKKTNBJBS2L7B2LUKSHSMEJWFG4KIS3FI'
        },
        { 
            no:         8,
            mnemonic:   'dog dog dog dog dog dog dog dog dog dog dog dose',
            method:     wallets.atomic.method,
            path:       wallets.atomic.path,
            key:        '0eed13381c206469210932dd7f58b0a84b9d44b1b63e9f963b0d4c4d1baead3f',
            address:    'CWEAA3OJTGY2IJOACHISLWAJNR6XMFNRLCD7MXRPFUBESTMMKSQ42XRBOI'
        },
        

    ]
    vectors.reduce((p, v, arr) => {
        return p.then(() => deriveMnemonicTest(v))
    },Promise.resolve())
    .catch(console.log)
}

const wallets = {
        atomic  :{ method: 'bip39-seed'      ,path: undefined           },
        coinomi :{ method: 'slip10-ed25519'  ,path: "m/44'/283'/0'/0/0" },
        exodus  :{ method: 'slip10-secp256k1',path: "m/44'/283'/0'/0/0" },
        ledger  :{ method: 'kholaw-ed25519'  ,path: "m/44'/283'/0'/0/0" },
        trust   :{ method: 'slip10-ed25519'  ,path: "m/44'/283'/0'/0/0" },
    }


//-------------------------------------------------------
//::EXAMPLE::
//-------------------------------------------------------
// mnemonic = 'all all all all all all all all all all all all all all all all all all all all all all all feel'
// deriveMnemonic(mnemonic, wallets.ledger.method, wallets.ledger.path)
// .then(node => {
//     console.log(node.algo.key)
//     console.log(node.algo.address)
//     words = prettifyWordsTTB(node.algo.words)
//     console.log(words)
// })

//-------------------------------------------------------
//::GENERATE DUMMY MNEMONICS FOR TESTING::
//-------------------------------------------------------
// console.log(testMnemonicWords('dog',12).join(' '))
// console.log(testMnemonicWords('boy',15).join(' '))
// console.log(testMnemonicWords('bar',24).join(' '))

//-------------------------------------------------------
//::RUN TEST VECTORS::
//-------------------------------------------------------
// tests()

module.exports = {
    algoWords,
    algoAddressFromMnemonic,
    bip39seed,
    deriveBip39Seed,
    deriveMnemonic,
    findBip39Word,
    parseMnemonic,
    prettifyWordsTTB,
    randomAlgoAddress,
    randomWords,
    tests,
    wallets,
}