into its own distinct package. It is used for client-side implementations of
Nano cryptocurrency wallets and enables building web-based applications that can
work even while offline. `libnemo` supports managing wallets, deriving accounts,
- signing blocks, and more.
+signing blocks, and more.
It utilizes the Web Crypto API which is native to all modern browsers. Private
keys are encrypted in storage with a user password as soon as they are derived,
## Features
-* Generate new BIP-32 hierarchial deterministic (HD) wallets with a BIP-39
-mnemonic phrase and the Nano path registered with BIP-44. Used by Ledger
-hardware wallet.
-* Generate new BLAKE2b wallets with a BIP-39 mnemonic phrases. Original method
-described by nano spec.
-* Import wallets with a mnemonic phrase or a seed.
-* Derive indexed accounts with a Nano address and a public-private keypair.
-* Create, sign, and verify send, receive, and change blocks.
-* Get account info and process blocks on the network while online.
-* Manage known addresses with a rolodex.
-* Sign and verify arbitrary strings with relevant keys.
-* Validate seeds, mnemonic phrases, and Nano addresses.
-* Convert Nano unit denominations.
+- Generate new BIP-32 hierarchial deterministic (HD) wallets with a BIP-39
+ mnemonic phrase and the Nano path registered with BIP-44. Used by Ledger
+ hardware wallet.
+- Generate new BLAKE2b wallets with a BIP-39 mnemonic phrases. Original method
+ described by nano spec.
+- Import wallets with a mnemonic phrase or a seed.
+- Derive indexed accounts with a Nano address and a public-private keypair.
+- Create, sign, and verify send, receive, and change blocks.
+- Get account info and process blocks on the network while online.
+- Manage known addresses with a rolodex.
+- Sign and verify arbitrary strings with relevant keys.
+- Validate seeds, mnemonic phrases, and Nano addresses.
+- Convert Nano unit denominations.
## Installation
For clarity, the following terms are used throughout the library:
- * BIP-32 - Defines how hierarchical determinstic (HD) wallets are generated
- * BIP-39 - Defines how mnemonic phrases are generated
- * BIP-44 - Expands on BIP-32 to define how an enhanced derivation path can
- allow a single wallet to store multiple currencies
+- BIP-32 - Defines how hierarchical determinstic (HD) wallets are generated
+- BIP-39 - Defines how mnemonic phrases are generated
+- BIP-44 - Expands on BIP-32 to define how an enhanced derivation path can
+ allow a single wallet to store multiple currencies
`libnemo` is able to generate and import HD and BLAKE2b wallets, and it can
derive accounts for both. An HD wallet seed is 64 bytes (128 hexadecimal
```javascript
try {
- await wallet.unlock(password)
+ await wallet.unlock(password);
} catch (err) {
- console.error(err)
+ console.error(err);
}
-const firstAccount = await wallet.account()
-const secondAccount = await wallet.account(1)
-const multipleAccounts = await wallet.accounts(2, 3)
-const thirdAccount = multipleAccounts[2]
-const { address, publicKey, index } = firstAccount
+const firstAccount = await wallet.account();
+const secondAccount = await wallet.account(1);
+const multipleAccounts = await wallet.accounts(2, 3);
+const thirdAccount = multipleAccounts[2];
+const { address, publicKey, index } = firstAccount;
-const nodeUrl = 'https://nano-node.example.com/'
-await firstAccount.refresh(nodeUrl) // online
-const { frontier, balance, representative } = firstAccount
+const nodeUrl = "https://nano-node.example.com/";
+await firstAccount.refresh(nodeUrl); // online
+const { frontier, balance, representative } = firstAccount;
```
### Blocks
subtypes based on the data they contain: send, receive, or change
representative. `libnemo` implements three methods to handle them appropriately:
-* `send(recipient, amount)`: the Nano balance of the account decreases
-* `receive(hash, amount)`: the Nano balance of the account increases and
-requires a matching send block hash
-* `change(representative)`: the representative for the account changes while the
-Nano balance does not
+- `send(recipient, amount)`: the Nano balance of the account decreases
+- `receive(hash, amount)`: the Nano balance of the account increases and
+ requires a matching send block hash
+- `change(representative)`: the representative for the account changes while the
+ Nano balance does not
_Nano protocol allows changing the representative at the same time as a balance
change. `libnemo` does not implement this for purposes of clarity; all change
using the
[account_info RPC command](https://docs.nano.org/commands/rpc-protocol/#account_info)
which can then be used to populate the block parameters. This can be done on a
-per-account basis with the `account.refresh()` method.
+per-account basis with the `account.refresh()` method or for a range of accounts
+with the `wallet.refresh()` method.
Blocks require a small proof-of-work that must be calculated for the block to be
accepted by the network. This can be provided when creating the block, generated
[work_generate RPC command](https://docs.nano.org/commands/rpc-protocol/#work_generate).
Finally, the block must be signed with the private key of the account. `libnemo`
-wallets, accounts, and blocks can all create signatures, event offline if
+wallets, accounts, and blocks can all create signatures, even offline if
desired. After being signed, the block can be published to the network with the
`block.process()` method or by separately calling out to the
[process RPC command](https://docs.nano.org/commands/rpc-protocol/#process).
#### Creating blocks
```javascript
-import { Block } from 'libnemo'
+import { Block } from "libnemo";
const sendBlock = new Block(
- 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // sender
- '5618869000000000000000000000000', // current balance
- '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block
- 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou' // representative
-)
+ "nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d", // sender
+ "5618869000000000000000000000000", // current balance
+ "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D", // hash of previous block
+ "nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou" // representative
+);
sendBlock.send(
- 'nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz', // recipient
- '2000000000000000000000000000000' // amount to send
-)
+ "nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz", // recipient
+ "2000000000000000000000000000000" // amount to send
+);
await sendBlock.pow(
- 'fbffed7c73b61367' // PoW nonce (argument is optional)
-)
-await sendBlock.sign(wallet, accountIndex) // signature added to block
+ "fbffed7c73b61367" // PoW nonce (argument is optional)
+);
+await sendBlock.sign(wallet, accountIndex); // signature added to block
await sendBlock.process(
- 'https://nano-node.example.com' // must be online
-)
+ "https://nano-node.example.com" // must be online
+);
const receiveBlock = await new Block(
- 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // recipient
- '18618869000000000000000000000000', // current balance
- '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block
- 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou' // representative
+ "nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d", // recipient
+ "18618869000000000000000000000000", // current balance
+ "92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D", // hash of previous block
+ "nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou" // representative
)
-.receive( // methods can be chained
- 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', // origin (hash of matching send block)
- '7000000000000000000000000000000', // amount that was sent
-)
-.pow(
- 'c5cf86de24b24419' // PoW nonce (synchronous if value provided)
-)
-.sign(wallet, accountIndex, frontier) // frontier may be necessary when using Ledger devices
+ .receive(
+ // methods can be chained
+ "CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783", // origin (hash of matching send block)
+ "7000000000000000000000000000000" // amount that was sent
+ )
+ .pow(
+ "c5cf86de24b24419" // PoW nonce (synchronous if value provided)
+ )
+ .sign(wallet, accountIndex, frontier); // frontier may be necessary when using Ledger devices
const changeBlock = await new Block(
- 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // account redelegating vote weight
- '3000000000000000000000000000000', // current balance
- '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4' // hash of previous block
-)
-.change(
- 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs' // new representative
-)
+ "nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d", // account redelegating vote weight
+ "3000000000000000000000000000000", // current balance
+ "128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4" // hash of previous block
+).change(
+ "nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs" // new representative
+);
sign(
- '1495F2D49159CC2EAAAA97EBB42346418E1268AFF16D7FCA90E6BAD6D0965520' // sign blocks with literal private keys too
-)
-.pow() // async if calculating locally
+ "1495F2D49159CC2EAAAA97EBB42346418E1268AFF16D7FCA90E6BAD6D0965520" // sign blocks with literal private keys too
+).pow(); // async if calculating locally
```
#### Signing a block with a wallet
```javascript
-const wallet = await Wallet.create('BIP-44', 'password123')
-await wallet.unlock('password123')
+const wallet = await Wallet.create("BIP-44", "password123");
+await wallet.unlock("password123");
try {
- await wallet.sign(0, block)
+ await wallet.sign(0, block);
} catch (err) {
- console.error(err)
+ console.error(err);
}
```
#### Signing a block with a private key
```javascript
-const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143'
+const privateKey =
+ "3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143";
try {
- await block.sign(privateKey)
+ await block.sign(privateKey);
} catch (err) {
- console.error(err)
+ console.error(err);
}
```
```javascript
try {
- await block.pow()
+ await block.pow();
} catch (err) {
- console.error(err)
+ console.error(err);
}
```
#### Requesting proof-of-work from an online service
```javascript
-const node = new Rpc('https://nano-node.example.com/')
+const node = new Rpc("https://nano-node.example.com/");
try {
- await block.pow('https://nano-node.example.com/')
+ await block.pow("https://nano-node.example.com/");
} catch (err) {
- console.error(err)
+ console.error(err);
}
```
#### Processing a block on the network
```javascript
-const node = new Rpc('https://nano-node.example.com', 'nodes-api-key')
+const node = new Rpc("https://nano-node.example.com", "nodes-api-key");
try {
- const hash = await block.process('https://nano-node.example.com/')
+ const hash = await block.process("https://nano-node.example.com/");
} catch (err) {
- console.error(err)
+ console.error(err);
}
```
represented by the primitive `bigint` data type. Other supported denominations
are as follows:
-| Unit | Raw |
-|-------|-----|
+| Unit | Raw |
+| ----- | ------------------- |
| RAI | 10<sup>24</sup> raw |
| NYANO | 10<sup>24</sup> raw |
| KRAI | 10<sup>27</sup> raw |
| MNANO | 10<sup>36</sup> raw |
```javascript
-import { Tools } from 'libnemo'
+import { Tools } from "libnemo";
// Denominations are case-insensitive
-const oneNanoToRaw = Tools.convert('1', 'NANO', 'RAW') // 1000000000000000000000000000000
-const oneNonillionRawToNano = Tools.convert('1000000000000000000000000000000', 'RAW', 'NANO') // 1
-const oneThousandNyanoToPico = Tools.convert('1000', 'nYaNo', 'pico') //1
-const oneThousandPicoToNano = Tools.convert('1000', 'pico', 'NANO') // 1
+const oneNanoToRaw = Tools.convert("1", "NANO", "RAW"); // 1000000000000000000000000000000
+const oneNonillionRawToNano = Tools.convert(
+ "1000000000000000000000000000000",
+ "RAW",
+ "NANO"
+); // 1
+const oneThousandNyanoToPico = Tools.convert("1000", "nYaNo", "pico"); //1
+const oneThousandPicoToNano = Tools.convert("1000", "pico", "NANO"); // 1
```
#### Verifying signatures and signing anything with the private key
-Since cryptocurrencies like Nano uses asymmetric keys to sign and verify blocks
+Since cryptocurrencies like Nano use asymmetric keys to sign and verify blocks
and transactions, a Nano account itself can be used to sign arbitrary data
with its private key and verify signatures from other accounts with their public
-keys.
+keys. For compatibility with other similar tools, `libnemo` will first hash the
+data to a 32-byte value using BLAKE2b and then sign the resulting digest.
For example, a client-side login can be implemented by challenging an account
-owner to sign their email address using their private key:
+owner to sign their email address:
```javascript
-import { Tools } from 'libnemo'
+// sign an arbitrary string
+const wallet = await Wallet.load("BIP-44", "some_password", seedToImport);
+await wallet.unlock("some_password");
+const account = await wallet.account(0);
-const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143'
-const publicKey = '5B65B0E8173EE0802C2C3E6C9080D1A16B06DE1176C938A924F58670904E82C4'
-const signature = await Tools.sign(privateKey, 'johndoe@example.com')
-const isValid = await Tools.verify(publicKey, signature, 'johndoe@example.com')
+const data = "johndoe@example.com";
+const signature = await wallet.sign(account.index, data);
+
+const isValid = await Tools.verify(account.publicKey, signature, data);
+console.log(isValid);
+```
+
+If the user has their private key, they can use it directly to sign too:
+
+```javascript
+import { Tools } from "libnemo";
+
+const privateKey =
+ "3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143";
+const publicKey =
+ "5B65B0E8173EE0802C2C3E6C9080D1A16B06DE1176C938A924F58670904E82C4";
+const signature = await Tools.sign(privateKey, "johndoe@example.com");
+const isValid = await Tools.verify(publicKey, signature, "johndoe@example.com");
```
Ownership of a Nano address can also be proven by challenging the account owner
account address.
```javascript
-import { Tools } from 'libnemo'
+import { Tools } from "libnemo";
-const address = 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d'
-const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143'
-const randomData = crypto.getRandomValues(new Uint8Array(32))
+const address =
+ "nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d";
+const privateKey =
+ "3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143";
+const randomData = crypto.getRandomValues(new Uint8Array(32));
-const signature = await Tools.sign(privateKey, ...randomData)
-const publicKey = new Account(address).publicKey
-const isValid = await Tools.verify(publicKey, signature, ...randomData)
+const signature = await Tools.sign(privateKey, ...randomData);
+const publicKey = new Account(address).publicKey;
+const isValid = await Tools.verify(publicKey, signature, ...randomData);
```
#### Validate a Nano account address
```javascript
-import { Tools } from 'libnemo'
+import { Tools } from "libnemo";
-const valid = Account.validate('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d')
+const valid = Account.validate(
+ "nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d"
+);
```
## Tests
Test vectors were retrieved from the following publicly-available locations:
- * Nano (BIP-44): https://docs.nano.org/integration-guides/key-management/#test-vectors
- * Trezor (BIP-39): https://github.com/trezor/python-mnemonic/blob/master/vectors.json
- * BIP-32: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors
+- Nano (BIP-44): https://docs.nano.org/integration-guides/key-management/#test-vectors
+- Trezor (BIP-39): https://github.com/trezor/python-mnemonic/blob/master/vectors.json
+- BIP-32: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors
Another set of test vectors were created for libnemo based on the Trezor set.
These extra test vectors were generated purely to test uncommon yet valid
## Building
-* `npm run build`: compile and build
-* `npm run test`: all of the above, run tests, and print results to the console
-* `npm run test:coverage`: all of the above, calculate code coverage, and print
-code coverage to the console
-* `npm run test:coverage:report`: all of the above, and open an HTML code
-coverage report in the browser (requires lcov and xdg-open)
+- `npm run build`: compile and build
+- `npm run test`: all of the above, run tests, and print results to the console
+- `npm run test:coverage`: all of the above, calculate code coverage, and print
+ code coverage to the console
+- `npm run test:coverage:report`: all of the above, and open an HTML code
+ coverage report in the browser (requires lcov and xdg-open)
## Donations