"url": "git+https://zoso.dev/libnemo.git"
},
"scripts": {
- "build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js --outdir=dist --target=es2022 --format=esm --platform=browser --bundle --minify --sourcemap",
+ "build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js --outdir=dist --target=es2022 --format=esm --platform=node --bundle --minify --sourcemap",
"test": "npm run build && node --test --env-file .env",
"test:coverage": "npm run test -- --experimental-test-coverage",
"test:coverage:report": "npm run test:coverage -- --test-reporter=lcov --test-reporter-destination=coverage.info && genhtml coverage.info --output-directory test/coverage && rm coverage.info && xdg-open test/coverage/index.html"
--- /dev/null
+// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import { Account } from './account.js'
+import { nanoCKD } from './bip32-key-derivation.js'
+import { bytes, dec } from './convert.js'
+import Tools from './tools.js'
+import type { Ledger } from './ledger.js'
+
+/**
+* Derives BIP-44 Nano account private keys.
+*
+* @param {number} index - Index of the account
+* @returns {Promise<Account>}
+*/
+onmessage = async (event) => {
+ let result = null
+ const { type, seed, index } = event.data
+ switch (type) {
+ case 'bip44': {
+ result = await ckdBip44(seed, index)
+ break
+ }
+ case 'blake2b': {
+ result = await ckdBlake2b(seed, index)
+ break
+ }
+ case 'ledger': {
+ result = await ckdLedger(seed, index)
+ break
+ }
+ }
+ postMessage(result)
+}
+
+/**
+* Derives BIP-44 Nano account private keys.
+*
+* @param {number} index - Index of the account
+* @returns {Promise<Account>}
+*/
+async function ckdBip44 (seed: string, index: number): Promise<Account> {
+ const key = await nanoCKD(seed, index)
+ return await Account.fromPrivateKey(key, index)
+}
+
+/**
+* Derives BLAKE2b account private keys.
+*
+* @param {number} index - Index of the account
+* @returns {Promise<Account>}
+*/
+async function ckdBlake2b (seed: string, index: number): Promise<Account> {
+ const hash = await Tools.blake2b([seed, dec.toHex(index, 4)])
+ const key = bytes.toHex(hash)
+ return await Account.fromPrivateKey(key, index)
+}
+
+/**
+* Gets the public key for an account from the Ledger device.
+*
+* @param {number} index - Index of the account
+* @returns {Promise<Account>}
+*/
+export async function ckdLedger (ledger: Ledger, index: number): Promise<Account | null> {
+ const { status, publicKey } = await ledger.account(index)
+ if (status === 'OK' && publicKey != null) {
+ return await Account.fromPublicKey(publicKey, index)
+ }
+ return null
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+const Worker = globalThis.Worker ?? (await import('node:worker_threads')).Worker
+/**
+* Assigns a Web Worker to process data. Creates a new one if none are available.
+*
+* @param {object} data - Arbitrary data encapsulated in a JSON object
+* @param {string} signature - Hexadcimal-formatted signature
+* @param {...string} input - Data to be verified
+* @returns {boolean} True if the data was signed by the public key's matching private key
+*/
+export class Pool {
+ #threads
+ #url
+
+ constructor (url: string | URL) {
+ this.#url = new URL(url, import.meta.url)
+ this.#threads = [...Array(navigator.hardwareConcurrency)]
+ this.#threads.forEach(slot => {
+ slot = {
+ worker: new Worker(this.#url),
+ tasks: [],
+ get isAvailable () { return this.tasks.length === 0 }
+ }
+ })
+ }
+
+ async work (data: object): Promise<any> {
+ return new Promise((resolve) => {
+ const thread = this.#threads.reduce((curr, next) => {
+ next.tasks.length < curr.tasks.length
+ ? next
+ : curr
+ })
+ thread.tasks.push(data)
+ thread.worker.postMessage(thread.tasks.shift())
+ thread.worker.onmessage = (event: any) => {
+ if (thread.tasks.length > 0) {
+ thread.worker.postMessage(thread.tasks.shift())
+ }
+ resolve(event.data)
+ }
+ })
+ }
+}
return bytes.toHex(hash)
}
-
-/**
-* Checks the endianness of the current machine.
-*
-* @returns {Promise<boolean>} True if little-endian, else false
-*/
-export async function littleEndian () {
- const buffer = new ArrayBuffer(2)
- new DataView(buffer).setUint16(0, 256, true)
- return new Uint16Array(buffer)[0] === 256
-}
-
/**
* Signs arbitrary strings with a private key using the Ed25519 signature scheme.
*
hex.toBytes(signature))
}
-export default { blake2b, convert, hash, littleEndian, sign, sweep, verify }
+export default { blake2b, convert, hash, sign, sweep, verify }
import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js'\r
import { bytes, dec } from './convert.js'\r
import { Entropy } from './entropy.js'\r
+import { Pool } from './pool.js'\r
import { Rpc } from './rpc.js'\r
import { Safe } from './safe.js'\r
import Tools from './tools.js'\r
import type { Ledger } from './ledger.js'\r
\r
+const ckdPool = new Pool('ckd.js')\r
+\r
/**\r
* Represents a wallet containing numerous Nano accounts derived from a single\r
* source, the form of which can vary based on the type of wallet. The Wallet\r
* @returns {Promise<Account>}\r
*/\r
async ckd (index: number): Promise<Account> {\r
- const key = await nanoCKD(this.seed, index)\r
- return await Account.fromPrivateKey(key, index)\r
+ return await ckdPool.work({ type: 'bip44', seed: this.seed, index })\r
}\r
}\r
\r
* @returns {Promise<Account>}\r
*/\r
async ckd (index: number): Promise<Account> {\r
- const indexBytes = dec.toBytes(index, 4)\r
- if (await Tools.littleEndian()) {\r
- indexBytes.reverse()\r
- }\r
- const hash = await Tools.blake2b([this.seed, bytes.toHex(indexBytes)])\r
- const key = bytes.toHex(hash)\r
- return await Account.fromPrivateKey(key, index)\r
+ return await ckdPool.work({ type: 'blake2b', seed: this.seed, index })\r
}\r
}\r
\r
* @returns True if successfully locked\r
*/\r
async lock (): Promise<boolean> {\r
- if (this.#ledger == null) {\r
+ if (this.ledger == null) {\r
return false\r
}\r
- const result = await this.#ledger.close()\r
+ const result = await this.ledger.close()\r
return result === 'OK'\r
}\r
\r
* @returns True if successfully unlocked\r
*/\r
async unlock (): Promise<boolean> {\r
- if (this.#ledger == null) {\r
+ if (this.ledger == null) {\r
return false\r
}\r
- const result = await this.#ledger.connect()\r
+ const result = await this.ledger.connect()\r
return result === 'OK'\r
}\r
}\r