From: Chris Duncan Date: Tue, 9 Sep 2025 04:34:45 +0000 (-0700) Subject: Update description and README. X-Git-Tag: v0.10.5~24^2~1 X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=4320680de5f8c7f87958b57643f3f4a2f2aa112a;p=libnemo.git Update description and README. --- diff --git a/README.md b/README.md index 8b90593..3ac08f3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later 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, @@ -19,19 +19,19 @@ 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. +- 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 @@ -53,10 +53,10 @@ 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 +- 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 @@ -86,20 +86,20 @@ const wallet = await Wallet.load('BLAKE2b', password, mnemonic) ```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 @@ -115,11 +115,11 @@ 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 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 @@ -129,7 +129,8 @@ 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. 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 @@ -138,7 +139,7 @@ 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` -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). @@ -146,75 +147,75 @@ desired. After being signed, the block can be published to the network with the #### 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); } ``` @@ -222,31 +223,31 @@ try { ```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); } ``` @@ -258,8 +259,8 @@ 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 | -|-------|-----| +| Unit | Raw | +| ----- | ------------------- | | RAI | 1024 raw | | NYANO | 1024 raw | | KRAI | 1027 raw | @@ -270,31 +271,53 @@ are as follows: | MNANO | 1036 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 @@ -302,32 +325,36 @@ to sign an arbitrary string and then validating the signature with the Nano 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 @@ -337,12 +364,12 @@ mnemonic phrase lengths like 15 or 18 words. ## 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 diff --git a/package.json b/package.json index e8e76d3..c32c7e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libnemo", "version": "0.6.0", - "description": "Asynchronous, non-blocking Nano cryptocurrency integration toolkit.", + "description": "Nano cryptocurrency wallet library.", "keywords": [ "nemo", "nano",