libnemo
Version:
Asynchronous, non-blocking Nano cryptocurrency integration toolkit.
340 lines (266 loc) • 12.7 kB
Markdown
<!--
SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
SPDX-License-Identifier: GPL-3.0-or-later
-->
# libnemo
`libnemo` is a fork of the nanocurrency-web toolkit. 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.
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,
and they are not exposed to other processes unless specifically exported by the
user. Optionally, Ledger device dependencies can be installed to enable Ledger
hardware wallet support.
## 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 entropy, seeds, mnemonic phrases, and Nano addresses.
* Convert Nano unit denominations.
## Installation
### From NPM
```console
npm install libnemo
```
## Usage
#### ⚠️ The examples below should never be used for real transactions! ⚠️
### Wallets and accounts
At its core, a wallet is a hexadecimal string called a seed. From this seed,
millions of unique accounts can be deterministically derived. The first account
in a wallet starts at index 0.
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
`libnemo` is able to generate and import HD and BLAKE2b wallets, and it can
derive accounts for both. An HD wallet seed is 128 characters while a BLAKE2b
wallet seed is 64 characters. For enhanced security, `libnemo` requires a
password to create or import wallets, and wallets are initialized in a locked
state. Implementations can provide their own Uint8Array bytes instead of a
password. Refer to the documentation on each class factory method for specific
usage.
```javascript
import { Bip44Wallet, Blake2bWallet } from 'libnemo'
const wallet = await Bip44Wallet.create(password)
const wallet = await Bip44Wallet.fromEntropy(password, entropy, salt?)
const wallet = await Bip44Wallet.fromMnemonic(password, mnemonic, salt?)
const wallet = await Bip44Wallet.fromSeed(password, seed)
const wallet = await Bip44Wallet.create(password)
const wallet = await Bip44Wallet.fromSeed(password, seed)
const wallet = await Bip44Wallet.fromMnemonic(password, mnemonic)
```
```javascript
try {
const unlockResult = await wallet.unlock(password)
} catch(err) {
console.log(err)
}
console.log(unlockResult) // true if successfully unlocked
const { mnemonic, seed } = wallet
const firstAccount = await wallet.account()
const secondAccount = await wallet.account(1)
const multipleAccounts = await wallet.accounts(2, 3)
const thirdAccount = multipleAccounts[3]
const { address, publicKey, index } = firstAccount
const nodeUrl = 'https://nano-node.example.com/'
await firstAccount.refresh(nodeUrl) // online
const { frontier, balance, representative } = firstAccount
```
### Blocks
Blocks do not contain transaction amounts. Instead, they contain stateful
balance changes only. For example, if sending Ӿ5 from an account with a balance
of Ӿ20, the send block would contain `balance: Ӿ15` (psuedocode for
demonstration purposes and not a literal depiction). This can be difficult to
track, so `libnemo` provides the convenience of specifying an amount to send or
receive and calculates the balance change itself.
All blocks are 'state' types, but they are interpreted as one of three different
subtypes based on the data they contain: send, receive, or change
representative. `libnemo` implements them as the following classes:
* SendBlock: the Nano balance of the account decreases
* ReceivBlock: the Nano balance of the account increases and requires a matching
SendBlock
* ChangeBlock: 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
ChangeBlock objects will maintain the same Nano balance._
Always fetch the most up to date information for the account from the network
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.
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
with the `block.pow()` method, or a requested from a public node that allows the
[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`
accounts can sign blocks offline if desired. After being signed, the block can
be published to the network with the
[process RPC command](https://docs.nano.org/commands/rpc-protocol/#process).
#### Creating blocks
```javascript
import { SendBlock, ReceiveBlock, ChangeBlock } from 'libnemo'
const send = new SendBlock(
'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // sender
'5618869000000000000000000000000', // current balance
'nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz', // recipient
'2000000000000000000000000000000', // amount to send
'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative
'92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block
'fbffed7c73b61367' // PoW nonce (optional at first but required to process)
)
const receive = new ReceiveBlock(
'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // recipient
'18618869000000000000000000000000', // current balance
'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', // origin (hash of matching send block)
'7000000000000000000000000000000', // amount that was sent
'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative
'92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block
'c5cf86de24b24419' // PoW nonce (optional at first but required to process)
)
const change = new ChangeBlock(
'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // account redelegating vote weight
'3000000000000000000000000000000', // current balance
'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', // new representative
'128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', // hash of previous block
'0000000000000000' // PoW nonce (optional at first but required to process)
)
```
#### Signing a block with a wallet
```javascript
const wallet = await Bip44Wallet.create('password123')
await wallet.unlock('password123')
try {
await wallet.sign(0, block)
} catch (err) {
console.log(err)
}
```
#### Signing a block with a detached account
```javascript
const account = await Account.import({privateKey: K, index: 0}, 'password123')
try {
await account.sign(block, 'password123')
} catch (err) {
console.log(err)
}
```
#### Signing a block with a private key
```javascript
const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143'
try {
await block.sign(privateKey)
} catch (err) {
console.log(err)
}
```
#### Calculating proof-of-work locally
```javascript
try {
await block.pow()
} catch (err) {
console.log(err)
}
```
#### Requesting proof-of-work from an online service
```javascript
const node = new Rpc('https://nano-node.example.com/')
try {
await block.pow('https://nano-node.example.com/')
} catch (err) {
console.log(err)
}
```
#### Processing a block on the network
```javascript
const node = new Rpc('https://nano-node.example.com', 'nodes-api-key')
try {
const hash = await block.process('https://nano-node.example.com/')
} catch (err) {
console.log(err)
}
```
### Tools
#### Converting Nano denominations
Raw values are the native unit of exchange throughout libnemo and are
represented by the primitive bigint data type. Other supported denominations
are as follows:
| Unit | Raw |
|-------|-----|
| RAI | 10<sup>24</sup> raw |
| NYANO | 10<sup>24</sup> raw |
| KRAI | 10<sup>27</sup> raw |
| PICO | 10<sup>27</sup> raw |
| MRAI | 10<sup>30</sup> raw |
| NANO | 10<sup>30</sup> raw |
| KNANO | 10<sup>33</sup> raw |
| MNANO | 10<sup>36</sup> raw |
```javascript
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
```
#### Verifying signatures and signing anything with the private key
Since cryptocurrencies like Nano uses 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.
For example, a client-side login can be implemented by challenging an account
owner to sign their email address using their private key:
```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
to sign an arbitrary string and then validating the signature with the Nano
account address.
```javascript
import { Tools } from 'libnemo'
const address = 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d'
const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143'
const randomData = new Entropy().hex
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'
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
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
mnemonic phrase lengths like 15 or 18 words.
#### ⚠️ The test vectors should never be used for real transactions! ⚠️
## 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)
## Donations
If you find this library helpful, please consider tipping the developer.
```
nano_1zosoqs47yt47bnfg7sdf46kj7asn58b7uzm9ek95jw7ccatq37898u1zoso
```