Version 1.3.0

* Add the possibility to generate legacy seeds and mnemonics
* Add the possibility to import legacy mnemonic phrases
* Stop throwing errors if given an upper case hex as input
* Return mnemonic also when creating wallet with a legacy seed
* npm audit fix
* Small refactoring
This commit is contained in:
Miro Metsänheimo 2021-01-12 22:30:58 +02:00
commit 1cf9dbf08f
9 changed files with 633 additions and 1046 deletions

View file

@ -6,13 +6,14 @@
Toolkit for Nano cryptocurrency client side offline implementations allowing you to build web- and mobile applications using Nano without compromising the user's keys by sending them out of their own device. Toolkit for Nano cryptocurrency client side offline implementations allowing you to build web- and mobile applications using Nano without compromising the user's keys by sending them out of their own device.
The toolkit supports creating and importing wallets and signing blocks on-device. Meaning that the user's keys should never be required to leave the device. The toolkit supports creating and importing wallets and signing blocks on-device. Meaning that the user's keys should never be required to leave the device. And much more!
## Features ## Features
* Generate wallets with a BIP32 mnemonic phrase * Generate wallets with a BIP32 mnemonic phrase
* BIP39/44 private key derivation * Generate wallets with legacy Nano mnemonic phrases
* Mnemonic is compatible with the Ledger Nano implementation * BIP32/44 private key derivation
* BIP39 Mnemonic is the same one which Ledger uses in their hardware wallets
* Import wallets with a mnemonic phrase or a seed * Import wallets with a mnemonic phrase or a seed
* Import wallets with the legacy Nano hex seed * Import wallets with the legacy Nano hex seed
* Sign send, receive and change representative blocks with a private key * Sign send, receive and change representative blocks with a private key
@ -44,9 +45,16 @@ import { wallet } from 'nanocurrency-web'
// Notice, that losing the password will make the mnemonic phrase void // Notice, that losing the password will make the mnemonic phrase void
const wallet = wallet.generate(entropy?, password?) const wallet = wallet.generate(entropy?, password?)
// Generates a legacy wallet with a mnemonic phrase, seed and an account
// You can provide your own seed to be used instead
const wallet = wallet.generateLegacey(seed?)
// Import a wallet with the mnemonic phrase // Import a wallet with the mnemonic phrase
const wallet = wallet.fromMnemonic(mnemonic, seedPassword?) const wallet = wallet.fromMnemonic(mnemonic, seedPassword?)
// Import a wallet with the legacy mnemonic phrase
const wallet = wallet.fromLegacyMnemonic(mnemonic)
// Import a wallet with a seed // Import a wallet with a seed
const wallet = wallet.fromSeed(seed) const wallet = wallet.fromSeed(seed)
@ -210,7 +218,7 @@ const valid = tools.validateMnemonic('edge defense waste choose enrich upon flee
### In web ### In web
```html ```html
<script src="https://unpkg.com/nanocurrency-web@1.2.2" type="text/javascript"></script> <script src="https://unpkg.com/nanocurrency-web@1.3.0" type="text/javascript"></script>
<script type="text/javascript"> <script type="text/javascript">
NanocurrencyWeb.wallet.generate(...); NanocurrencyWeb.wallet.generate(...);
</script> </script>

View file

@ -40,6 +40,25 @@ const wallet = {
return generator.generateWallet(entropy, seedPassword) return generator.generateWallet(entropy, seedPassword)
}, },
/**
* Generate a new Nano cryptocurrency wallet
*
* This function generates a legacy wallet from a random seed. Wallet includes
* a mnemonic phrase and a seed, the account is derived from the seed at index 0.
*
* The Nano address is derived from the public key using standard Nano encoding.
* The address is prefixed with 'nano_'.
*
* Generation uses CryptoJS to generate random seed by default. You can give your own seed
* as a parameter and it will be used instead.
*
* @param {string} [seed] - (Optional) 64 byte hexadecimal string seed to be used instead of generating
* @returns the generated mnemonic, seed and account
*/
generateLegacy: (seed: string): Wallet => {
return generator.generateLegacyWallet(seed)
},
/** /**
* Import a Nano cryptocurrency wallet from a mnemonic phrase * Import a Nano cryptocurrency wallet from a mnemonic phrase
* *
@ -59,6 +78,23 @@ const wallet = {
return importer.fromMnemonic(mnemonic, seedPassword) return importer.fromMnemonic(mnemonic, seedPassword)
}, },
/**
* Import a Nano cryptocurrency wallet from a legacy mnemonic phrase
*
* This function imports a wallet from an old Nano mnemonic phrase. Wallet includes the mnemonic
* phrase, a seed which represents the mnemonic and an account derived from the seed at index 0.
*
* The Nano address is derived from the public key using standard Nano encoding.
* The address is prefixed with 'nano_'.
*
* @param {string} mnemonic - The mnemonic phrase. Words are separated with a space
* @throws Throws an error if the mnemonic phrase doesn't pass validations
* @returns the wallet derived from the mnemonic (mnemonic, seed, account)
*/
fromLegacyMnemonic: (mnemonic: string): Wallet => {
return importer.fromLegacyMnemonic(mnemonic)
},
/** /**
* Import a Nano cryptocurrency wallet from a seed * Import a Nano cryptocurrency wallet from a seed
* *

View file

@ -1,40 +1,35 @@
import Bip32KeyDerivation from './bip32-key-derivation' import AddressImporter, { Wallet } from './address-importer'
import Bip39Mnemonic from './bip39-mnemonic' import Bip39Mnemonic from './bip39-mnemonic'
import Ed25519 from './ed25519'
import NanoAddress from './nano-address'
import { Wallet } from './address-importer'
export default class AddressGenerator { export default class AddressGenerator {
/** /**
* Generates the wallet * Generates a hierarchial deterministic BIP32/39/44 wallet
* *
* @param {string} [entropy] - (Optional) Custom entropy if the caller doesn't want a default generated entropy * @param {string} [entropy] - (Optional) Custom entropy if the caller doesn't want a default generated entropy
* @param {string} [seedPassword] - (Optional) Password for the seed * @param {string} [seedPassword] - (Optional) Password for the seed
*/ */
generateWallet(entropy = '', seedPassword: string = ''): Wallet { generateWallet(entropy = '', seedPassword: string = ''): Wallet {
const bip39 = new Bip39Mnemonic(seedPassword) const bip39 = new Bip39Mnemonic(seedPassword)
const wallet = bip39.createWallet(entropy) const mnemonicSeed = bip39.createWallet(entropy)
const wallet = new AddressImporter().fromSeed(mnemonicSeed.seed, 0, 0)
const bip44 = new Bip32KeyDerivation(`44'/165'/0'`, wallet.seed)
const privateKey = bip44.derivePath().key
const ed25519 = new Ed25519()
const keyPair = ed25519.generateKeys(privateKey)
const nano = new NanoAddress()
const address = nano.deriveAddress(keyPair.publicKey)
return { return {
mnemonic: wallet.mnemonic, ...wallet,
seed: wallet.seed, mnemonic: mnemonicSeed.mnemonic,
accounts: [{
accountIndex: 0,
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey,
address,
}],
} }
} }
/**
* Generates a legacy Nano wallet
*
*/
generateLegacyWallet(seed?: string): Wallet {
const bip39 = new Bip39Mnemonic()
const mnemonicSeed = bip39.createLegacyWallet(seed)
const wallet = new AddressImporter().fromLegacySeed(mnemonicSeed.seed, 0, 0, mnemonicSeed.mnemonic)
return wallet
}
} }

View file

@ -21,7 +21,29 @@ export default class AddressImporter {
} }
const seed = bip39.mnemonicToSeed(mnemonic) const seed = bip39.mnemonicToSeed(mnemonic)
return this.nano(seed, 0, 0, mnemonic) const accounts = this.accounts(seed, 0, 0)
return {
mnemonic,
seed,
accounts,
}
}
/**
* Import a legacy wallet using a mnemonic phrase
*
* @param {string} mnemonic - The mnemonic words to import the wallet from
* @returns {Wallet} - The wallet derived from the mnemonic phrase
*/
fromLegacyMnemonic(mnemonic: string): Wallet {
const bip39 = new Bip39Mnemonic()
if (!bip39.validateMnemonic(mnemonic)) {
throw new Error('Invalid mnemonic phrase')
}
const seed = bip39.mnemonicToLegacySeed(mnemonic)
return this.fromLegacySeed(seed, 0, 0, mnemonic)
} }
/** /**
@ -46,11 +68,17 @@ export default class AddressImporter {
if (seed.length !== 128) { if (seed.length !== 128) {
throw new Error('Invalid seed length, must be a 128 byte hexadecimal string') throw new Error('Invalid seed length, must be a 128 byte hexadecimal string')
} }
if (!/^[0-9a-f]+$/i.test(seed)) { if (!/^[0-9a-fA-F]+$/i.test(seed)) {
throw new Error('Seed is not a valid hexadecimal string') throw new Error('Seed is not a valid hexadecimal string')
} }
return this.nano(seed, from, to, undefined) const accounts = this.accounts(seed, from, to)
return {
mnemonic: undefined,
seed,
accounts,
}
} }
@ -62,43 +90,30 @@ export default class AddressImporter {
* @param {number} [to] - (Optional) The end index of the private keys to derive to * @param {number} [to] - (Optional) The end index of the private keys to derive to
* @returns {Wallet} The wallet derived from the seed * @returns {Wallet} The wallet derived from the seed
*/ */
fromLegacySeed(seed: string, from: number = 0, to: number = 0): Wallet { fromLegacySeed(seed: string, from: number = 0, to: number = 0, mnemonic?: string): Wallet {
const signer = new Signer() if (seed.length !== 64) {
throw new Error('Invalid seed length, must be a 64 byte hexadecimal string')
const accounts: Account[] = [] }
for (let i = from; i <= to; i++) { if (!/^[0-9a-fA-F]+$/i.test(seed)) {
const privateKey = Convert.ab2hex(signer.generateHash([seed, Convert.dec2hex(i, 4)])) throw new Error('Seed is not a valid hexadecimal string')
const ed25519 = new Ed25519()
const keyPair = ed25519.generateKeys(privateKey)
const nano = new NanoAddress()
const address = nano.deriveAddress(keyPair.publicKey)
accounts.push({
accountIndex: i,
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey,
address,
})
} }
const accounts = this.legacyAccounts(seed, from, to)
return { return {
mnemonic: undefined, mnemonic: mnemonic || new Bip39Mnemonic().deriveMnemonic(seed),
seed, seed,
accounts, accounts,
} }
} }
/** /**
* Derives the private keys * Derives BIP32 private keys
* *
* @param {string} seed - The seed to use for private key derivation * @param {string} seed - The seed to use for private key derivation
* @param {number} from - The start index of private keys to derive from * @param {number} from - The start index of private keys to derive from
* @param {number} to - The end index of private keys to derive to * @param {number} to - The end index of private keys to derive to
* @param {string} [mnemonic] - (Optional) the mnemonic phrase to return with the wallet
*/ */
private nano(seed: string, from: number, to: number, mnemonic?: string): Wallet { private accounts(seed: string, from: number, to: number): Account[] {
const accounts = [] const accounts = []
for (let i = from; i <= to; i++) { for (let i = from; i <= to; i++) {
@ -119,11 +134,37 @@ export default class AddressImporter {
}) })
} }
return { return accounts
mnemonic, }
seed,
accounts, /**
* Derives legacy private keys
*
* @param {string} seed - The seed to use for private key derivation
* @param {number} from - The start index of private keys to derive from
* @param {number} to - The end index of private keys to derive to
*/
private legacyAccounts(seed: string, from: number, to: number): Account[] {
const signer = new Signer()
const accounts: Account[] = []
for (let i = from; i <= to; i++) {
const privateKey = Convert.ab2hex(signer.generateHash([seed, Convert.dec2hex(i, 4)]))
const ed25519 = new Ed25519()
const keyPair = ed25519.generateKeys(privateKey)
const nano = new NanoAddress()
const address = nano.deriveAddress(keyPair.publicKey)
accounts.push({
accountIndex: i,
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey,
address,
})
} }
return accounts
} }
} }

View file

@ -14,7 +14,7 @@ export default class Bip39Mnemonic {
} }
/** /**
* Creates a new wallet * Creates a BIP39 wallet
* *
* @param {string} [entropy] - (Optional) the entropy to use instead of generating * @param {string} [entropy] - (Optional) the entropy to use instead of generating
* @returns {MnemonicSeed} The mnemonic phrase and a seed derived from the (generated) entropy * @returns {MnemonicSeed} The mnemonic phrase and a seed derived from the (generated) entropy
@ -24,7 +24,7 @@ export default class Bip39Mnemonic {
if (entropy.length !== 64) { if (entropy.length !== 64) {
throw new Error('Invalid entropy length, must be a 64 byte hexadecimal string') throw new Error('Invalid entropy length, must be a 64 byte hexadecimal string')
} }
if (!/^[0-9a-f]+$/i.test(entropy)) { if (!/^[0-9a-fA-F]+$/i.test(entropy)) {
throw new Error('Entopy is not a valid hexadecimal string') throw new Error('Entopy is not a valid hexadecimal string')
} }
} }
@ -33,6 +33,44 @@ export default class Bip39Mnemonic {
entropy = this.randomHex(64) entropy = this.randomHex(64)
} }
const mnemonic = this.deriveMnemonic(entropy)
const seed = this.mnemonicToSeed(mnemonic)
return {
mnemonic,
seed,
}
}
/**
* Creates an old Nano wallet
*
* @param {string} seed - (Optional) the seed to be used for the wallet
* @returns {MnemonicSeed} The mnemonic phrase and a generated seed if none provided
*/
createLegacyWallet = (seed?: string): MnemonicSeed => {
if (seed) {
if (seed.length !== 64) {
throw new Error('Invalid entropy length, must be a 64 byte hexadecimal string')
}
if (!/^[0-9a-fA-F]+$/i.test(seed)) {
throw new Error('Entopy is not a valid hexadecimal string')
}
}
if (!seed) {
seed = this.randomHex(64)
}
const mnemonic = this.deriveMnemonic(seed)
return {
mnemonic,
seed,
}
}
deriveMnemonic = (entropy: string): string => {
const entropyBinary = Convert.hexStringToBinary(entropy) const entropyBinary = Convert.hexStringToBinary(entropy)
const entropySha256Binary = Convert.hexStringToBinary(this.calculateChecksum(entropy)) const entropySha256Binary = Convert.hexStringToBinary(this.calculateChecksum(entropy))
const entropyBinaryWithChecksum = entropyBinary + entropySha256Binary const entropyBinaryWithChecksum = entropyBinary + entropySha256Binary
@ -42,13 +80,7 @@ export default class Bip39Mnemonic {
mnemonicWords.push(words[parseInt(entropyBinaryWithChecksum.substr(i, 11), 2)]) mnemonicWords.push(words[parseInt(entropyBinaryWithChecksum.substr(i, 11), 2)])
} }
const mnemonicFinal = mnemonicWords.join(' ') return mnemonicWords.join(' ')
const seed = this.mnemonicToSeed(mnemonicFinal)
return {
mnemonic: mnemonicFinal,
seed,
}
} }
/** /**
@ -68,7 +100,7 @@ export default class Bip39Mnemonic {
if (wordIndex === -1) { if (wordIndex === -1) {
return false return false
} }
return (wordIndex.toString(2)).padStart(11, '0') return (Convert.dec2bin(wordIndex)).padStart(11, '0')
}).join('') }).join('')
const dividerIndex = Math.floor(bits.length / 33) * 32 const dividerIndex = Math.floor(bits.length / 33) * 32
@ -76,9 +108,17 @@ export default class Bip39Mnemonic {
const checksumBits = bits.slice(dividerIndex) const checksumBits = bits.slice(dividerIndex)
const entropyBytes = entropyBits.match(/(.{1,8})/g).map((bin: string) => parseInt(bin, 2)) const entropyBytes = entropyBits.match(/(.{1,8})/g).map((bin: string) => parseInt(bin, 2))
if (entropyBytes.length < 16) return false if (entropyBytes.length < 16) {
if (entropyBytes.length > 32) return false return false
if (entropyBytes.length % 4 !== 0) return false }
if (entropyBytes.length > 32) {
return false
}
if (entropyBytes.length % 4 !== 0) {
return false
}
const entropyHex = Convert.bytesToHexString(entropyBytes) const entropyHex = Convert.bytesToHexString(entropyBytes)
const newChecksum = this.calculateChecksum(entropyHex) const newChecksum = this.calculateChecksum(entropyHex)
@ -91,6 +131,34 @@ export default class Bip39Mnemonic {
return true return true
} }
/**
* Converts the mnemonic phrase to an old Nano seed
*
* @param {string} mnemonic Mnemonic phrase separated by spaces
*/
mnemonicToLegacySeed = (mnemonic: string): string => {
const wordArray = Util.normalizeUTF8(mnemonic).split(' ')
const bits = wordArray.map((w: string) => {
const wordIndex = words.indexOf(w)
if (wordIndex === -1) {
return false
}
return (Convert.dec2bin(wordIndex)).padStart(11, '0')
}).join('')
const dividerIndex = Math.floor(bits.length / 33) * 32
const entropyBits = bits.slice(0, dividerIndex)
const entropyBytes = entropyBits.match(/(.{1,8})/g).map((bin: string) => parseInt(bin, 2))
const entropyHex = Convert.bytesToHexString(entropyBytes)
return entropyHex
}
/**
* Converts the mnemonic phrase to a BIP39 seed
*
* @param {string} mnemonic Mnemonic phrase separated by spaces
*/
mnemonicToSeed = (mnemonic: string): string => { mnemonicToSeed = (mnemonic: string): string => {
const normalizedMnemonic = Util.normalizeUTF8(mnemonic) const normalizedMnemonic = Util.normalizeUTF8(mnemonic)
const normalizedPassword = 'mnemonic' + Util.normalizeUTF8(this.password) const normalizedPassword = 'mnemonic' + Util.normalizeUTF8(this.password)

View file

@ -93,12 +93,16 @@ export default class Convert {
return joined return joined
} }
static dec2bin = (dec: number): string => {
return (dec >>> 0).toString(2)
}
static bytesToHexString = (bytes: number[]): string => { static bytesToHexString = (bytes: number[]): string => {
return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('') return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('')
} }
static hexStringToBinary = (hex: string): string => { static hexStringToBinary = (hex: string): string => {
return [...hex].map(c => (parseInt(c, 16).toString(2)).padStart(4, '0')).join('') return [...hex].map(c => (Convert.dec2bin(parseInt(c, 16))).padStart(4, '0')).join('')
} }
static binaryToHexString = (bin: string): string => { static binaryToHexString = (bin: string): string => {

1366
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "nanocurrency-web", "name": "nanocurrency-web",
"version": "1.2.2", "version": "1.3.0",
"description": "Toolkit for Nano cryptocurrency client side offline integrations", "description": "Toolkit for Nano cryptocurrency client side offline integrations",
"author": "Miro Metsänheimo <miro@metsanheimo.fi>", "author": "Miro Metsänheimo <miro@metsanheimo.fi>",
"license": "MIT", "license": "MIT",

View file

@ -56,7 +56,7 @@ describe('generate wallet test', () => {
}) })
// Test vectors from https://docs.nano.org/integration-guides/key-management/ // Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere
describe('import wallet with test vectors test', () => { describe('import wallet with test vectors test', () => {
it('should successfully import a wallet with the official Nano test vectors mnemonic', () => { it('should successfully import a wallet with the official Nano test vectors mnemonic', () => {
@ -94,7 +94,7 @@ describe('import wallet with test vectors test', () => {
expect(result).to.have.own.property('mnemonic') expect(result).to.have.own.property('mnemonic')
expect(result).to.have.own.property('seed') expect(result).to.have.own.property('seed')
expect(result).to.have.own.property('accounts') expect(result).to.have.own.property('accounts')
expect(result.mnemonic).to.be.undefined expect(result.mnemonic).to.equal('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art')
expect(result.seed).to.equal('0000000000000000000000000000000000000000000000000000000000000000') expect(result.seed).to.equal('0000000000000000000000000000000000000000000000000000000000000000')
expect(result.accounts[0].privateKey).to.equal('9f0e444c69f77a49bd0be89db92c38fe713e0963165cca12faf5712d7657120f') expect(result.accounts[0].privateKey).to.equal('9f0e444c69f77a49bd0be89db92c38fe713e0963165cca12faf5712d7657120f')
expect(result.accounts[0].publicKey).to.equal('c008b814a7d269a1fa3c6528b19201a24d797912db9996ff02a1ff356e45552b') expect(result.accounts[0].publicKey).to.equal('c008b814a7d269a1fa3c6528b19201a24d797912db9996ff02a1ff356e45552b')
@ -123,6 +123,19 @@ describe('import wallet with test vectors test', () => {
expect(() => wallet.generate('0gc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c')).to.throw(Error) expect(() => wallet.generate('0gc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c')).to.throw(Error)
}) })
it('should successfully create a new legacy wallet and get the same result from importing one from the mnemonic', () => {
const result = wallet.generateLegacy('BE3E51EE51BAB11950B2495013512FEB110D9898B4137DA268709621CE2862F4')
expect(result).to.have.own.property('mnemonic')
expect(result).to.have.own.property('seed')
expect(result).to.have.own.property('accounts')
expect(result.mnemonic).to.equal('sail verb knee pet prison million drift empty exotic once episode stomach awkward slush glare list laundry battle bring clump brother before mesh pair')
const imported = wallet.fromLegacyMnemonic(result.mnemonic)
expect(imported.mnemonic).to.equal(result.mnemonic)
expect(imported.seed.toUpperCase()).to.equal(result.seed)
expect(imported.accounts[0].privateKey).to.equal(result.accounts[0].privateKey)
})
}) })
describe('derive more accounts from the same seed test', () => { describe('derive more accounts from the same seed test', () => {