Version 1.1.0

* Separate block signer to it's own class
* Add JSDOC to some functions
* Renamed converter to tools
* Added the ability to sign any strings with the private key
This commit is contained in:
Miro Metsänheimo 2020-03-07 13:43:11 +02:00
commit c3648758bc
13 changed files with 1091 additions and 941 deletions

View file

@ -165,19 +165,31 @@ const signedBlock = block.representative(data, privateKey)
Supported unit values are RAW, NANO, MRAI, KRAI, RAW.
```javascript
import { converter } from 'nanocurrency-web'
import { tools } from 'nanocurrency-web'
// Convert 1 Nano to RAW
const converted = converter.convert('1', 'NANO', 'RAW')
const converted = tools.convert('1', 'NANO', 'RAW')
// Convert 1 RAW to Nano
const converted = converter.convert('1000000000000000000000000000000', 'RAW', 'NANO')
const converted = tools.convert('1000000000000000000000000000000', 'RAW', 'NANO')
```
#### Signing any data with the private key
For example implementing client side login with the password being the user's e-mail signed with their private key
```javascript
import { tools } from 'nanocurrency-web'
const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3';
const signed = tools.sign(privateKey, 'foo@bar.com')
```
### In web
```html
<script src="https://unpkg.com/nanocurrency-web@1.0.6" type="text/javascript"></script>
<script src="https://unpkg.com/nanocurrency-web@1.1.0" type="text/javascript"></script>
<script type="text/javascript">
NanocurrencyWeb.wallet.generate(...);
</script>

View file

@ -3,9 +3,12 @@ import AddressImporter, { Account, Wallet } from './lib/address-importer'
import BlockSigner, { SendBlock, ReceiveBlock, RepresentativeBlock, SignedBlock } from './lib/block-signer'
import BigNumber from 'bignumber.js'
import NanoConverter from './lib/nano-converter'
import Signer from './lib/signer'
import Convert from './lib/util/convert'
const generator = new AddressGenerator()
const importer = new AddressImporter()
const signer = new Signer();
const wallet = {
/**
@ -162,7 +165,7 @@ const block = {
}
const converter = {
const tools = {
/**
* Convert Nano values
@ -173,14 +176,25 @@ const converter = {
* @param {string} inputUnit The unit of the input value
* @param {string} outputUnit The unit you wish to convert to
*/
convert: (input: string | BigNumber, inputUnit: string, outputUnit: string) => {
convert: (input: string | BigNumber, inputUnit: string, outputUnit: string): string => {
return NanoConverter.convert(input, inputUnit, outputUnit)
},
/**
* Sign any strings with the user's private key
*
* @param {string} privateKey The private key to sign with
* @param {...string} input Data to sign
*/
sign: (privateKey: string, ...input: string[]): string => {
const data = input.map(Convert.stringToHex)
return signer.sign(privateKey, ...data);
},
}
export {
wallet,
block,
converter,
tools,
}

View file

@ -9,7 +9,8 @@ export default class AddressGenerator {
/**
* Generates the wallet
*
* @param {String} seedPassword Password for the seed
* @param {string} [entropy] - (Optional) Custom entropy if the caller doesn't want a default generated entropy
* @param {string} [seedPassword] - (Optional) Password for the seed
*/
generateWallet(entropy = '', seedPassword: string = ''): Wallet {
const bip39 = new Bip39Mnemonic(seedPassword)

View file

@ -5,6 +5,13 @@ import NanoAddress from './nano-address'
export default class AddressImporter {
/**
* Import a wallet using a mnemonic phrase
*
* @param {string} mnemonic - The mnemonic words to import the wallet from
* @param {string} [seedPassword] - (Optional) The password to use to secure the mnemonic
* @returns {Wallet} - The wallet derived from the mnemonic phrase
*/
fromMnemonic(mnemonic: string, seedPassword = ''): Wallet {
const bip39 = new Bip39Mnemonic(seedPassword)
if (!bip39.validateMnemonic(mnemonic)) {
@ -15,6 +22,14 @@ export default class AddressImporter {
return this.nano(seed, 0, 0, mnemonic)
}
/**
* Import a wallet using a seed
*
* @param {string} seed - The seed to import the wallet from
* @param {number} [from] - (Optional) The start index of the private keys to derive from
* @param {number} [to] - (Optional) The end index of the private keys to derive to
* @returns {Wallet} The wallet derived from the mnemonic phrase
*/
fromSeed(seed: string, from = 0, to = 0): Wallet {
if (seed.length !== 128) {
throw new Error('Invalid seed length, must be a 128 byte hexadecimal string')
@ -27,8 +42,12 @@ export default class AddressImporter {
}
/**
* Generates the wallet
* @param {String} seedPassword Password for the seed
* Derives the 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
* @param {string} [mnemonic] - (Optional) the mnemonic phrase to return with the wallet
*/
private nano(seed: string, from: number, to: number, mnemonic?: string): Wallet {
const accounts = []

View file

@ -13,7 +13,13 @@ export default class Bip39Mnemonic {
this.password = password
}
createWallet = (entropy: string): { mnemonic: string, seed: string } => {
/**
* Creates a new wallet
*
* @param {string} [entropy] - (Optional) the entropy to use instead of generating
* @returns {MnemonicSeed} The mnemonic phrase and a seed derived from the (generated) entropy
*/
createWallet = (entropy: string): MnemonicSeed => {
if (entropy) {
if (entropy.length !== 64) {
throw new Error('Invalid entropy length, must be a 64 byte hexadecimal string')
@ -45,6 +51,12 @@ export default class Bip39Mnemonic {
}
}
/**
* Validates a mnemonic phrase
*
* @param {string} mnemonic - The mnemonic phrase to validate
* @returns {boolean} Is the mnemonic phrase valid
*/
validateMnemonic = (mnemonic: string): boolean => {
const wordArray = Util.normalizeUTF8(mnemonic).split(' ')
if (wordArray.length % 3 !== 0) {
@ -104,3 +116,8 @@ export default class Bip39Mnemonic {
}
}
interface MnemonicSeed {
mnemonic: string,
seed: string,
}

View file

@ -1,19 +1,28 @@
import BigNumber from 'bignumber.js'
import Ed25519 from './ed25519'
import Convert from './util/convert'
import NanoAddress from './nano-address'
import NanoConverter from './nano-converter'
import Signer from './signer'
import BigNumber from 'bignumber.js'
//@ts-ignore
import { blake2b, blake2bInit, blake2bUpdate, blake2bFinal } from 'blakejs'
import { blake2b } from 'blakejs'
export default class BlockSigner {
nanoAddress = new NanoAddress()
ed25519 = new Ed25519()
signer = new Signer()
preamble = 0x6.toString().padStart(64, '0')
preamble: string = 0x6.toString().padStart(64, '0')
/**
* Sign a receive block
*
* @param {ReceiveBlock} data The data required to sign a receive block
* @param {string} privateKey Private key to sign the data with
* @returns {SignedBlock} the signed block to publish to the blockchain
*/
receive(data: ReceiveBlock, privateKey: string): SignedBlock {
const validateInputRaw = (input: string) => !!input && !isNaN(+input)
if (!validateInputRaw(data.walletBalanceRaw)) {
@ -57,9 +66,14 @@ export default class BlockSigner {
const link = data.transactionHash
const representative = this.nanoAddressToHexString(data.representativeAddress)
const signatureBytes = this.ed25519.sign(
this.generateHash(this.preamble, account, data.frontier, representative, newBalanceHex, link),
Convert.hex2ab(privateKey))
const signature = this.signer.sign(
privateKey,
this.preamble,
account,
data.frontier,
representative,
newBalanceHex,
link)
return {
type: 'state',
@ -68,11 +82,18 @@ export default class BlockSigner {
representative: data.representativeAddress,
balance: newBalanceRaw,
link: link,
signature: Convert.ab2hex(signatureBytes),
signature: signature,
work: data.work,
}
}
/**
* Sign a send block
*
* @param {SendBlock} data The data required to sign a send block
* @param {string} privateKey Private key to sign the data with
* @returns {SignedBlock} the signed block to publish to the blockchain
*/
send(data: SendBlock, privateKey: string): SignedBlock {
const validateInputRaw = (input: string) => !!input && !isNaN(+input)
if (!validateInputRaw(data.walletBalanceRaw)) {
@ -96,11 +117,11 @@ export default class BlockSigner {
}
if (!data.frontier) {
throw new Error('No frontier')
throw new Error('Frontier is not set')
}
if (!data.work) {
throw new Error('No work')
throw new Error('Work is not set')
}
if (!privateKey) {
@ -116,9 +137,14 @@ export default class BlockSigner {
const link = this.nanoAddressToHexString(data.toAddress)
const representative = this.nanoAddressToHexString(data.representativeAddress)
const signatureBytes = this.ed25519.sign(
this.generateHash(this.preamble, account, data.frontier, representative, newBalanceHex, link),
Convert.hex2ab(privateKey))
const signature = this.signer.sign(
privateKey,
this.preamble,
account,
data.frontier,
representative,
newBalanceHex,
link)
return {
type: 'state',
@ -127,22 +153,11 @@ export default class BlockSigner {
representative: data.representativeAddress,
balance: newBalanceRaw,
link: link,
signature: Convert.ab2hex(signatureBytes),
signature: signature,
work: data.work,
}
}
private generateHash(preamble: string, account: string, previous: string, representative: string, balance: string, link: string) {
const ctx = blake2bInit(32, undefined)
blake2bUpdate(ctx, Convert.hex2ab(preamble))
blake2bUpdate(ctx, Convert.hex2ab(account))
blake2bUpdate(ctx, Convert.hex2ab(previous))
blake2bUpdate(ctx, Convert.hex2ab(representative))
blake2bUpdate(ctx, Convert.hex2ab(balance))
blake2bUpdate(ctx, Convert.hex2ab(link))
return blake2bFinal(ctx)
}
private nanoAddressToHexString(addr: string): string {
addr = addr.slice(-60)
const isValid = /^[13456789abcdefghijkmnopqrstuwxyz]+$/.test(addr)
@ -154,9 +169,9 @@ export default class BlockSigner {
const key = Convert.ab2hex(keyBytes).toUpperCase()
return key
}
throw new Error('Checksum mismatch')
throw new Error('Checksum mismatch in address')
} else {
throw new Error('Illegal characters')
throw new Error('Illegal characters in address')
}
}

35
lib/signer.ts Normal file
View file

@ -0,0 +1,35 @@
import Convert from './util/convert'
import Ed25519 from './ed25519'
//@ts-ignore
import { blake2bInit, blake2bUpdate, blake2bFinal } from 'blakejs'
export default class Signer {
ed25519 = new Ed25519()
/**
* Signs any data using the ed25519 signature system
*
* @param privateKey Private key to sign the data with
* @param data Data to sign
*/
sign(privateKey: string, ...data: string[]): string {
const signature = this.ed25519.sign(
this.generateHash(data),
Convert.hex2ab(privateKey))
return Convert.ab2hex(signature)
}
/**
* Creates a blake2b hash of the input data
*
* @param data Data to hash
*/
private generateHash(data: string[]): Uint8Array {
const ctx = blake2bInit(32, undefined)
data.forEach(str => blake2bUpdate(ctx, Convert.hex2ab(str)))
return blake2bFinal(ctx)
}
}

View file

@ -105,4 +105,8 @@ export default class Convert {
return parseInt(bin, 2).toString(16)
}
static stringToHex = (str: string): string => {
return [...str].map(c => c.charCodeAt(0).toString(16)).join('')
}
}

1813
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "nanocurrency-web",
"version": "1.0.6",
"version": "1.1.0",
"description": "Toolkit for Nano cryptocurrency client side offline integrations",
"author": "Miro Metsänheimo <miro@metsanheimo.fi>",
"license": "MIT",
@ -32,7 +32,7 @@
"dependencies": {
"bignumber.js": "9.0.0",
"blakejs": "1.1.0",
"crypto-js": "3.1.9-1"
"crypto-js": "4.0.0"
},
"devDependencies": {
"@types/node": "12.7.12",

View file

@ -1,7 +1,7 @@
'use strict'
const expect = require('chai').expect
const { wallet, block, converter } = require('../dist/index')
const { wallet, block, tools } = require('../dist/index')
// WARNING: Do not send any funds to the test vectors below
describe('generate wallet test', () => {
@ -158,13 +158,27 @@ describe('block signing tests using official test vectors', () => {
describe('unit conversion tests', () => {
it('should convert nano to raw', () => {
const result = converter.convert('1', 'NANO', 'RAW')
const result = tools.convert('1', 'NANO', 'RAW')
expect(result).to.equal('1000000000000000000000000000000')
})
it('should convert raw to nano', () => {
const result = converter.convert('1000000000000000000000000000000', 'RAW', 'NANO')
const result = tools.convert('1000000000000000000000000000000', 'RAW', 'NANO')
expect(result).to.equal('1.000000000000000')
})
})
describe('Signer tests', () => {
it('should sign data with a single parameter', () => {
const result = tools.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3', 'miro@metsanheimo.fi')
expect(result).to.equal('0ede9f287b7d58a053aa9ad84419c856ac39ec4c2453098ef19abf9638b07b1993e0cd3747723aada71602e92e781060dc3b91c410d32def1b4780a62fd0eb02')
})
it('should sign data with multiple parameters', () => {
const result = tools.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3', 'miro@metsanheimo.fi', 'somePassword')
expect(result).to.equal('a7b88357a160f54cf4db2826c86483eb60e66e8ccb36f9a37f3fb636c9d80f7b59d1fba88d0be27f85ac3fcbe5c6e13f911d7e5b713e86fb8e9a635932a2af05')
})
})