libnemo
v0.10.4
Published
Nano cryptocurrency wallet library.
Maintainers
Readme
libnemo
libnemo started as a fork of the nanocurrency-web toolkit and has evolved
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.
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 seeds, mnemonic phrases, and Nano addresses.
- Convert Nano unit denominations.
Installation
From NPM
npm install libnemoUsage
⚠️ 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 64 bytes (128 hexadecimal
characters), and a BLAKE2b wallet seed is 32 bytes (64 hexadecimal characters).
For enhanced security, libnemo requires a password to create or load wallets,
and wallets are initialized in a locked state. When importing an existing
wallet, the seed and, if included, the mnemonic phrase (collectively referenced
herein as "wallet secrets") are inaccessible; since the user provided them, the
user should already have a copy of them. For convenience, a verification method
can be used to compare a user-provided value to the wallet value and return true
if they are equal. When creating a new wallet, the wallet secrets are each
accessible once and self-destruct after the first access of each of their
values.
import { Wallet } from 'libnemo'
const wallet = await Wallet.create('BIP-44', password)
const wallet = await Wallet.load('BIP-44', password, mnemonic, salt?)
const wallet = await Wallet.load('BIP-44', password, seed)
const wallet = await Wallet.create('BLAKE2b', password)
const wallet = await Wallet.load('BLAKE2b', password, seed)
const wallet = await Wallet.load('BLAKE2b', password, mnemonic)try {
await wallet.unlock(password);
} catch (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 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 three methods to handle them appropriately:
send(recipient, amount): the Nano balance of the account decreasesreceive(hash, amount): the Nano balance of the account increases and requires a matching send block hashchange(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
block 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
which can then be used to populate the block parameters. This can be done on a
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
locally with the block.pow() method, or requested from a public node that
allows the
work_generate RPC command.
Finally, the block must be signed with the private key of the account. libnemo
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.
Creating blocks
import { Block } from "libnemo";
const sendBlock = new Block(
"nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d", // sender
"5618869000000000000000000000000", // current balance
"92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D", // hash of previous block
"nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou" // representative
);
sendBlock.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
await sendBlock.process(
"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
)
.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
);
sign(
"1495F2D49159CC2EAAAA97EBB42346418E1268AFF16D7FCA90E6BAD6D0965520" // sign blocks with literal private keys too
).pow(); // async if calculating locallySigning a block with a wallet
const wallet = await Wallet.create("BIP-44", "password123");
await wallet.unlock("password123");
try {
await wallet.sign(0, block);
} catch (err) {
console.error(err);
}Signing a block with a private key
const privateKey =
"3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143";
try {
await block.sign(privateKey);
} catch (err) {
console.error(err);
}Calculating proof-of-work locally
try {
await block.pow();
} catch (err) {
console.error(err);
}Requesting proof-of-work from an online service
const node = new Rpc("https://nano-node.example.com/");
try {
await block.pow("https://nano-node.example.com/");
} catch (err) {
console.error(err);
}Processing a block on the network
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.error(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 | 1024 raw | | NYANO | 1024 raw | | KRAI | 1027 raw | | PICO | 1027 raw | | MRAI | 1030 raw | | NANO | 1030 raw | | KNANO | 1033 raw | | MNANO | 1036 raw |
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"); // 1Verifying signatures and signing anything with the private key
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. 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:
// 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 data = "[email protected]";
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:
import { Tools } from "libnemo";
const privateKey =
"3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143";
const publicKey =
"5B65B0E8173EE0802C2C3E6C9080D1A16B06DE1176C938A924F58670904E82C4";
const signature = await Tools.sign(privateKey, "[email protected]");
const isValid = await Tools.verify(publicKey, signature, "[email protected]");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.
import { Tools } from "libnemo";
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);Validate a Nano account address
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 buildnpm run test: all of the above, run tests, and print results to the consolenpm run test:coverage: all of the above, calculate code coverage, and print code coverage to the consolenpm 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