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:
parent
ba06a44cf4
commit
c3648758bc
13 changed files with 1091 additions and 941 deletions
20
README.md
20
README.md
|
|
@ -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>
|
||||
|
|
|
|||
20
index.ts
20
index.ts
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
35
lib/signer.ts
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
1813
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
20
test/test.js
20
test/test.js
|
|
@ -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')
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue