--- /dev/null
+<!--
+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; as such,
+it has only a required dependency in order to work with the BLAKE2b algorithm.
+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.
+* Run in modern web browsers and mobile frameworks built with Javascript without
+server-side NodeJS functions.
+
+## 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. More
+advanced implementations can provide their own CryptoKey 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 accounts = await wallet.accounts(from?, to?)
+const firstAccount = accounts[0]
+const { address, publicKey, privateKey, 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, or a
+public node that allows the
+[work_generate RPC command](https://docs.nano.org/commands/rpc-protocol/#work_generate)
+can be used.
+
+Finally, the block must be signed with the private key of the account. libnemo
+enables this to be done 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
+```javascript
+const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143'
+try {
+ await block.sign(privateKey)
+} catch (err) {
+ console.log(err)
+}
+```
+
+#### Caculating 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
+```
'use strict'
-import { assert, average, print, skip, suite, test } from './CONFIG.mjs'
+import { assert, average, print, random, skip, suite, test } from './CONFIG.mjs'
import { NanoPowGl, NanoPowGpu } from '../dist/main.min.js'
await suite('Block performance', async () => {
- const COUNT = 0x20
+ const COUNT = 0xf
await test(`NanoPowGpu: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => {
const times = []
- const random = new Uint8Array(32)
for (let i = 0; i < COUNT; i++) {
- crypto.getRandomValues(random)
- const hash = random.reduce((curr, next) => { return `${curr}${next.toString(16).padStart(2, '0')}` }, '')
+ const hash = random()
const start = performance.now()
const work = await NanoPowGpu.search(hash)
const end = performance.now()
print(times)
})
- await skip(`NanoPowGl: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => {
+ await test(`NanoPowGl: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => {
const times = []
- const random = new Uint8Array(32)
for (let i = 0; i < COUNT; i++) {
- crypto.getRandomValues(random)
- const hash = random.reduce((curr, next) => { return `${curr}${next}` }, '')
+ const hash = random()
const start = performance.now()
const work = await NanoPowGl.search(hash)
const end = performance.now()
print(times)
})
- await skip(`nano-webgl-pow: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => {
+ await test(`nano-webgl-pow: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => {
//@ts-expect-error
window.NanoWebglPow.width = 256 * Math.max(1, Math.floor(navigator.hardwareConcurrency))
//@ts-expect-error
window.NanoWebglPow.height = 256 * Math.max(1, Math.floor(navigator.hardwareConcurrency))
const times = []
- const random = new Uint8Array(32)
for (let i = 0; i < COUNT; i++) {
- crypto.getRandomValues(random)
- const hash = random.reduce((curr, next) => { return `${curr}${next}` }, '')
+ const hash = random()
const start = performance.now()
const work = await new Promise(resolve => {
//@ts-expect-error