]> zoso.dev Git - nano-pow.git/commitdiff
Copy README from libnemo which will need to be rewritten. Add nano-webgl-js to devdep...
authorChris Duncan <chris@zoso.dev>
Fri, 10 Jan 2025 23:26:00 +0000 (15:26 -0800)
committerChris Duncan <chris@zoso.dev>
Fri, 10 Jan 2025 23:26:00 +0000 (15:26 -0800)
README.md [new file with mode: 0644]
package-lock.json
package.json
test/CONFIG.mjs
test/index.html
test/test.mjs

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..8a10272
--- /dev/null
+++ b/README.md
@@ -0,0 +1,282 @@
+<!--
+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
+```
index 1f7ebe82c7dfe01a12302771232a532a8c1d7e7a..9d89fcb04ea5b6a1a1b6ec4d7a26a761e824797f 100644 (file)
@@ -13,6 +13,7 @@
                                "@webgpu/types": "^0.1.52",
                                "esbuild": "^0.24.2",
                                "esbuild-plugin-glsl": "^1.2.2",
+                               "nano-webgl-pow": "^1.1.1",
                                "typescript": "^5.7.3"
                        },
                        "funding": {
                                "esbuild": "0.x.x"
                        }
                },
+               "node_modules/nano-webgl-pow": {
+                       "version": "1.1.1",
+                       "resolved": "https://registry.npmjs.org/nano-webgl-pow/-/nano-webgl-pow-1.1.1.tgz",
+                       "integrity": "sha512-IKAg7qx2y4n9dnT7tYYypOun/aV+35SfRxJCVnc63GboWQ5/woVIVAZcdX5VfXM1mLYBzADvXxoWZ39G3iPOFA==",
+                       "dev": true,
+                       "license": "MIT"
+               },
                "node_modules/typescript": {
                        "version": "5.7.3",
                        "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
index 8565097b6aa3aa81269064cfe1add28817cd7945..0006b2beaa270e6f9fcbfe037a6577b43e2e36a0 100644 (file)
@@ -51,6 +51,7 @@
                "@webgpu/types": "^0.1.52",
                "esbuild": "^0.24.2",
                "esbuild-plugin-glsl": "^1.2.2",
+               "nano-webgl-pow": "^1.1.1",
                "typescript": "^5.7.3"
        },
        "type": "module",
index 4ade381a2cbbefac60375677dbd2239f1b814810..7afb956f32ef1fcc3d79894e09942281a3ac21d9 100644 (file)
@@ -1,6 +1,14 @@
 // SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+export function random (size = 32) {
+       const bytes = new Uint8Array(size)
+       crypto.getRandomValues(bytes)
+       let hex = ''
+       for (let i = 0; i < size; i++) hex += bytes[i].toString(16).padStart(2, '0')
+       return hex
+}
+
 export function average (times) {
        let count = times.length, sum = 0, reciprocals = 0, logarithms = 0, truncated = 0, min = 0xffff, max = 0
        times.sort()
index 6e5156b6c3a9424454a5834dbfd440d4ba1441d0..3cb8c5d400fb834477a1c68956022679e8600bd2 100644 (file)
@@ -8,6 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 <head>
        <link rel="icon" href="./favicon.ico">
        <script type="module" src="./test.mjs"></script>
+       <script type="module" src="https://cdn.jsdelivr.net/npm/nano-webgl-pow@1.1.1/nano-webgl-pow.js"></script>
        <style>body{background:black;}</style>
 </head>
 
index 7f805c6e4581be2f82f8811458946c6dd22672a6..41b1d93a3d5727df8daf9ad09c6d14ceead32a78 100644 (file)
@@ -3,18 +3,16 @@
 
 '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()
@@ -24,12 +22,10 @@ await suite('Block performance', async () => {
                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()
@@ -39,16 +35,14 @@ await suite('Block performance', async () => {
                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