Skip to content

Commit

Permalink
Version 1.2.0
Browse files Browse the repository at this point in the history
* Import wallets with legacy Nano hex seeds
  • Loading branch information
numsu committed May 2, 2020
1 parent 8509da9 commit d0b804a
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 16 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The toolkit supports creating and importing wallets and signing blocks on-device
* BIP39/44 private key derivation
* Mnemonic is compatible with the Ledger Nano implementation
* Import wallets with a mnemonic phrase or a seed
* Import wallets with the legacy Nano hex seed
* Sign send, receive and change representative blocks with a private key
* Runs in all web browsers and mobile frameworks built with Javascript
* Convert Nano units
Expand Down Expand Up @@ -48,12 +49,18 @@ const wallet = wallet.fromMnemonic(mnemonic, seedPassword?)
// Import a wallet with a seed
const wallet = wallet.fromSeed(seed)

// Import a wallet with a legacy hex seed
const wallet = wallet.fromLegacySeed(seed)

// Derive private keys for a seed, from and to are number indexes
const accounts = wallet.accounts(seed, from, to)

// Derive private keys for a legacy seed, from and to are number indexes
const accounts = wallet.legacyAccounts(seed, from, to)
```
```javascript
// The returned wallet JSON format is as follows:
// The returned wallet JSON format is as follows. The mnemonic phrase will be undefined when importing with a seed.
{
mnemonic: 'edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur',
seed: '0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c',
Expand Down Expand Up @@ -177,7 +184,7 @@ 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
For example implementing client side login with the password being the user's e-mail signed with their private key. Make sure that you double check the signature on the back-end side with the public key.
```javascript
import { tools } from 'nanocurrency-web'
Expand All @@ -190,7 +197,7 @@ const signed = tools.sign(privateKey, '[email protected]')
### In web
```html
<script src="https://unpkg.com/nanocurrency-web@1.1.1" type="text/javascript"></script>
<script src="https://unpkg.com/nanocurrency-web@1.2.0" type="text/javascript"></script>
<script type="text/javascript">
NanocurrencyWeb.wallet.generate(...);
</script>
Expand Down
52 changes: 42 additions & 10 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const wallet = {
* An optional seed password can be used to encrypt the mnemonic phrase so the seed
* cannot be derived correctly without the password. Recovering the password is not possible.
*
* @param {string} [entropy] Optional 64 byte hexadecimal string entropy to be used instead of the default
* @param {string} [seedPassword] Optional seed password
* @param {string} [entropy] - (Optional) 64 byte hexadecimal string entropy to be used instead of the default
* @param {string} [seedPassword] - (Optional) seed password
* @returns the generated mnemonic, seed and account
*/
generate: (entropy?: string, seedPassword?: string): Wallet => {
Expand All @@ -46,10 +46,10 @@ const wallet = {
* 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
* @param {string} [seedPassword] Optional seed password
* @param {string} mnemonic - The mnemonic phrase. Words are separated with a space
* @param {string} [seedPassword] - (Optional) seed password
* @throws Throws an error if the mnemonic phrase doesn't pass validations
* @returns the imported mnemonic, seed and account
* @returns the wallet derived from the mnemonic (mnemonic, seed, account)
*/
fromMnemonic: (mnemonic: string, seedPassword?: string): Wallet => {
return importer.fromMnemonic(mnemonic, seedPassword)
Expand All @@ -65,28 +65,60 @@ const wallet = {
* The Nano address is derived from the public key using standard Nano encoding.
* The address is prefixed with 'nano_'.
*
* @param {string} seed The seed
* @returns the importes seed and account
* @param {string} seed - The seed
* @returns {Wallet} the wallet derived from the seed (seed, account)
*/
fromSeed: (seed: string): Wallet => {
return importer.fromSeed(seed)
},

/**
* Import Nano cryptocurrency accounts from a legacy hex seed
*
* This function imports a wallet from a seed. The private key is derived from the seed using
* simply a blake2b hash function. The public key is derived from the private key using the ed25519 curve
* algorithm.
*
* The Nano address is derived from the public key using standard Nano encoding.
* The address is prefixed with 'nano_'.
*
* @param {string} seed - The seed
* @returns the wallet derived from the seed (seed, account)
*
*/
fromLegacySeed: (seed: string): Wallet => {
return importer.fromLegacySeed(seed);
},

/**
* Derive accounts for the seed
*
* This function derives Nano accounts with the BIP32 deterministic hierarchial algorithm
* from the given seed with input parameters 44'/165' and indexes based on the from and to
* parameters.
*
* @param {string} seed The seed
* @param {number} from The start index
* @param {number} to The end index
* @param {string} seed - The seed
* @param {number} from - The start index
* @param {number} to - The end index
*/
accounts: (seed: string, from: number, to: number): Account[] => {
return importer.fromSeed(seed, from, to).accounts
},

/**
* Derive accounts for the legacy hex seed
*
* This function derives Nano accounts with the given seed with indexes
* based on the from and to parameters.
*
* @param {string} seed - The seed
* @param {number} from - The start index
* @param {number} to - The end index
*/
legacyAccounts: (seed: string, from: number, to: number): Account[] => {
return importer.fromLegacySeed(seed, from, to).accounts
},

}

const blockSigner = new BlockSigner()
Expand Down
40 changes: 40 additions & 0 deletions lib/address-importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Bip32KeyDerivation from './bip32-key-derivation'
import Bip39Mnemonic from './bip39-mnemonic'
import Ed25519 from './ed25519'
import NanoAddress from './nano-address'
import Signer from './signer'
import Convert from './util/convert'

export default class AddressImporter {

Expand Down Expand Up @@ -40,6 +42,43 @@ export default class AddressImporter {

return this.nano(seed, from, to, undefined)
}


/**
* Import a wallet using a legacy 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 seed
*/
fromLegacySeed(seed: string, from: number = 0, to: number = 0): Wallet {
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 {
mnemonic: undefined,
seed,
accounts,
}
}

/**
* Derives the private keys
Expand All @@ -61,6 +100,7 @@ export default class AddressImporter {

const nano = new NanoAddress()
const address = nano.deriveAddress(keyPair.publicKey)

accounts.push({
accountIndex: i,
privateKey: keyPair.privateKey,
Expand Down
2 changes: 1 addition & 1 deletion lib/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default class Signer {
*
* @param data Data to hash
*/
private generateHash(data: string[]): Uint8Array {
generateHash(data: string[]): Uint8Array {
const ctx = blake2bInit(32, undefined)
data.forEach(str => blake2bUpdate(ctx, Convert.hex2ab(str)))
return blake2bFinal(ctx)
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nanocurrency-web",
"version": "1.1.1",
"version": "1.2.0",
"description": "Toolkit for Nano cryptocurrency client side offline integrations",
"author": "Miro Metsänheimo <[email protected]>",
"license": "MIT",
Expand Down
25 changes: 25 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,31 @@ describe('import wallet with official test vectors test', () => {
expect(result.accounts[0].address).to.equal('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d')
})

it('should successfully import a legacy hex wallet with the a seed', () => {
const result = wallet.fromLegacySeed('0000000000000000000000000000000000000000000000000000000000000000')
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.be.undefined
expect(result.seed).to.equal('0000000000000000000000000000000000000000000000000000000000000000')
expect(result.accounts[0].privateKey).to.equal('9f0e444c69f77a49bd0be89db92c38fe713e0963165cca12faf5712d7657120f')
expect(result.accounts[0].publicKey).to.equal('c008b814a7d269a1fa3c6528b19201a24d797912db9996ff02a1ff356e45552b')
expect(result.accounts[0].address).to.equal('nano_3i1aq1cchnmbn9x5rsbap8b15akfh7wj7pwskuzi7ahz8oq6cobd99d4r3b7')
})

it('should successfully import legacy hex accounts with the a seed', () => {
const accounts = wallet.legacyAccounts('0000000000000000000000000000000000000000000000000000000000000000', 0, 3)
expect(accounts[0]).to.have.own.property('accountIndex')
expect(accounts[0]).to.have.own.property('privateKey')
expect(accounts[0]).to.have.own.property('publicKey')
expect(accounts[0]).to.have.own.property('address')
expect(accounts).to.have.lengthOf(4)
expect(accounts[2].accountIndex).to.equal(2)
expect(accounts[2].privateKey).to.equal('6a1804198020b080996ba45b5891f8227d7a4f41c8479824423780d234939d58')
expect(accounts[2].publicKey).to.equal('2fea520fe54f5d0dca79d553d9c7f5af7db6ac17586dbca6905794caadc639df')
expect(accounts[2].address).to.equal('nano_1dzcca9ycmtx3q79mocmu95zdduxptp3gp5fqkmb1ownscpweggzah8cb4rb')
})

it('should throw when given a seed with an invalid length', () => {
expect(() => wallet.generate('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310')).to.throw(Error)
expect(() => wallet.generate('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310cd')).to.throw(Error)
Expand Down

0 comments on commit d0b804a

Please sign in to comment.