"url": "git+https://zoso.dev/libnemo.git"
},
"scripts": {
- "build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js ckd=dist/lib/ckd.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 ckd=dist/lib/ckd.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"
// 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'
+
+if (globalThis.Worker == null) {
+ const { isMainThread, parentPort } = await import('node:worker_threads')
+ if (!isMainThread) {
+ globalThis.addEventListener ??= Object.getPrototypeOf(parentPort).on.bind(parentPort)
+ globalThis.postMessage ??= Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
+ }
+}
/**
* Derives BIP-44 Nano account private keys.
* @param {number} index - Index of the account
* @returns {Promise<Account>}
*/
-globalThis.onmessage = (event) => {
- const { type, seed, index } = event.data
+addEventListener('message', (message) => {
+ const { type, seed, index } = message.data ?? message
switch (type) {
case 'bip44': {
ckdBip44(seed, index).then(postMessage)
break
}
}
-}
+}, { once: true })
/**
* Derives BIP-44 Nano account private keys.
+++ /dev/null
-// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
-// SPDX-License-Identifier: GPL-3.0-or-later
-const Worker = globalThis.Worker
-
-type Task = {
- data: object,
- resolve: Function
-}
-
-class Thread {
- isAvailable: boolean
- worker: Worker
-
- constructor (url: string | URL) {
- this.isAvailable = true
- this.worker = new Worker(new URL(url, import.meta.url))
- }
-}
-
-/**
-* 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 {
- #tasks: any[]
- #threads: Thread[]
- #url
-
- constructor (url: string | URL) {
- this.#tasks = []
- this.#url = new URL(url, import.meta.url)
- this.#threads = Array(navigator.hardwareConcurrency)
- .fill(undefined)
- .map(() => { return new Thread(this.#url) })
- }
-
- #assign (thread: Thread, task: Task) {
- thread.isAvailable = false
- thread.worker.onmessage = (event) => {
- if (this.#tasks.length > 0) {
- const next = this.#tasks.shift()
- this.#assign(thread, next)
- } else {
- thread.isAvailable = true
- }
- task.resolve(event.data)
- }
- thread.worker.postMessage(task.data)
- }
-
- async work (data: object): Promise<any> {
- return new Promise(resolve => {
- const thread = this.#threads.find(t => t.isAvailable)
- if (thread) {
- this.#assign(thread, { data, resolve })
- } else {
- this.#tasks.push({ data, resolve })
- }
- })
- }
-}
import { buffer, hex, utf8 } from './convert.js'
import { Entropy } from './entropy.js'
const { subtle } = globalThis.crypto
-const storage = globalThis.sessionStorage
const ERR_MSG = 'Failed to store item in Safe'
export class Safe {
+ #storage = globalThis.sessionStorage
/**
* Encrypts data with a password and stores it in the Safe.
*/
async put (name: string, key: CryptoKey, data: any): Promise<boolean>
async put (name: string, passkey: string | CryptoKey, data: any): Promise<boolean> {
- if (storage.getItem(name)) {
+ if (this.#storage.getItem(name)) {
throw new Error(ERR_MSG)
}
return this.overwrite(name, passkey as string, data)
}
await new Promise<void>((resolve, reject) => {
try {
- storage.setItem(name, JSON.stringify(record))
+ this.#storage.setItem(name, JSON.stringify(record))
resolve()
} catch (err) {
reject(err)
} catch (err) {
throw new Error(ERR_MSG)
}
- return (storage.getItem(name) != null)
+ return (this.#storage.getItem(name) != null)
}
/**
}
const item = await new Promise<string | null>(resolve => {
- resolve(storage.getItem(name))
+ resolve(this.#storage.getItem(name))
})
if (item == null) {
return null
const decoded = buffer.toUtf8(decrypted)
const data = JSON.parse(decoded)
passkey = ''
- storage.removeItem(name)
+ this.#storage.removeItem(name)
return data
} catch (err) {
return null
\r
import { Account } from './account.js'\r
import { Bip39Mnemonic } from './bip39-mnemonic.js'\r
-import { nanoCKD } from './bip32-key-derivation.js'\r
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
*/\r
export class Bip44Wallet extends Wallet {\r
static #isInternal: boolean = false\r
+ #worker: Worker\r
\r
constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {\r
if (!Bip44Wallet.#isInternal) {\r
throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
}\r
super(seed, mnemonic, id)\r
+ this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' })\r
Bip44Wallet.#isInternal = false\r
}\r
\r
* @returns {Promise<Account>}\r
*/\r
async ckd (index: number): Promise<Account> {\r
- const key = await ckdPool.work({ type: 'bip44', seed: this.seed, index })\r
- if (typeof key !== 'string') {\r
- throw new TypeError('BIP-44 child key derivation returned invalid data')\r
- }\r
- return await Account.fromPrivateKey(key, index)\r
+ return new Promise(resolve => {\r
+ this.#worker.addEventListener('message', (message) => {\r
+ const key = message.data ?? message\r
+ if (typeof key !== 'string') {\r
+ throw new TypeError('BIP-44 child key derivation returned invalid data')\r
+ }\r
+ Account.fromPrivateKey(key, index).then(resolve)\r
+ }, { once: true })\r
+ this.#worker.postMessage({ type: 'bip44', seed: this.seed, index })\r
+ })\r
}\r
}\r
\r
*/\r
export class Blake2bWallet extends Wallet {\r
static #isInternal: boolean = false\r
+ #worker: Worker\r
\r
constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {\r
if (!Blake2bWallet.#isInternal) {\r
throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
}\r
super(seed, mnemonic, id)\r
+ this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' })\r
Blake2bWallet.#isInternal = false\r
}\r
\r
* @returns {Promise<Account>}\r
*/\r
async ckd (index: number): Promise<Account> {\r
- const key = await ckdPool.work({ type: 'blake2b', seed: this.seed, index })\r
- if (typeof key !== 'string') {\r
- throw new TypeError('BLAKE2b child key derivation returned invalid data')\r
- }\r
- return await Account.fromPrivateKey(key, index)\r
+ return new Promise(resolve => {\r
+ this.#worker.addEventListener('message', (message) => {\r
+ const key = message.data ?? message\r
+ if (typeof key !== 'string') {\r
+ throw new TypeError('BLAKE2b child key derivation returned invalid data')\r
+ }\r
+ Account.fromPrivateKey(key, index).then(resolve)\r
+ }, { once: true })\r
+ this.#worker.postMessage({ type: 'blake2b', seed: this.seed, index })\r
+ })\r
}\r
}\r
\r
// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
// SPDX-License-Identifier: GPL-3.0-or-later
-import { Worker } from 'node:worker_threads'
-
-globalThis.Worker ??= Worker
-globalThis.Worker.onmessage ??= (handler) => Worker.prototype.on('message', handler)
-globalThis.onmessage ??= (handler) => EventTarget.prototype.addEventListener('message', handler)
+if (globalThis.Worker == null) {
+ const { Worker } = await import('node:worker_threads')
+ Worker.prototype.addEventListener = Worker.prototype.on
+ globalThis.Worker = Worker
+}
if (globalThis.sessionStorage == null) {
const _sessionStorage = {}
\r
'use strict'\r
\r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
import { describe, it } from 'node:test'\r
import { strict as assert } from 'assert'\r
import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
\r
'use strict'\r
\r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
import { describe, it } from 'node:test'\r
import { strict as assert } from 'assert'\r
import { NANO_TEST_VECTORS } from './TEST_VECTORS.js'\r
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
- await it('should derive the first account from the given BIP-44 seed', async function () {\r
+ it('should derive the first account from the given BIP-44 seed', async function () {\r
const accounts = await wallet.accounts()\r
\r
assert.equal(accounts.length, 1)\r
assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0)\r
})\r
\r
- await it('should derive low indexed accounts from the given BIP-44 seed', async function () {\r
+ it('should derive low indexed accounts from the given BIP-44 seed', async function () {\r
const accounts = await wallet.accounts(1, 2)\r
\r
assert.equal(accounts.length, 2)\r
assert.equal(accounts[1].address, NANO_TEST_VECTORS.ADDRESS_2)\r
})\r
\r
- await it('should derive high indexed accounts from the given seed', async function () {\r
+ it('should derive high indexed accounts from the given seed', async function () {\r
const accounts = await wallet.accounts(0x70000000, 0x700000ff)\r
\r
assert.equal(accounts.length, 0x100)\r
}\r
})\r
\r
- await it('should derive accounts for a BLAKE2b wallet', async function () {\r
+ it('should derive accounts for a BLAKE2b wallet', async function () {\r
const bwallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)\r
await bwallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const lowAccounts = await bwallet.accounts(0, 2)\r
})\r
\r
describe('child key derivation performance', { skip }, async () => {\r
- await it('performance test of BIP-44 ckd', async function () {\r
+ it('performance test of BIP-44 ckd', async function () {\r
const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
assert.equal(accounts.length, 0x8000)\r
})\r
\r
- await it('performance test of BLAKE2b ckd', async function () {\r
+ it('performance test of BLAKE2b ckd', async function () {\r
const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
\r
'use strict'\r
\r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
import { describe, it } from 'node:test'\r
import { strict as assert } from 'assert'\r
import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r
})\r
\r
it('should successfully import a BLAKE2b wallet with Trezor test vectors', async () => {\r
- const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
- await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
+ const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)\r
+ await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const accounts = await wallet.accounts(0, 1)\r
\r
assert.ok('mnemonic' in wallet)\r
})\r
\r
it('should get identical BLAKE2b wallets when created with a seed versus with its derived mnemonic', async () => {\r
- const wallet = await Blake2bWallet.fromSeed(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)\r
- await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
+ const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)\r
+ await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const walletAccounts = await wallet.accounts()\r
const walletAccount = walletAccounts[0]\r
\r
})\r
\r
it('should get identical BLAKE2b wallets when created with max entropy value', async () => {\r
- const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
- await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)\r
+ const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)\r
+ await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
const accounts = await wallet.accounts()\r
\r
assert.ok('mnemonic' in wallet)\r
\r
'use strict'\r
\r
-import './GLOBALS.js'\r
+import './GLOBALS.mjs'\r
import { describe, it } from 'node:test'\r
import { strict as assert } from 'assert'\r
import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'\r