]> zoso.dev Git - libnemo.git/commitdiff
Get it working in browser, then work backward toward Node testing if possible.
authorChris Duncan <chris@zoso.dev>
Tue, 26 Nov 2024 12:54:28 +0000 (04:54 -0800)
committerChris Duncan <chris@zoso.dev>
Tue, 26 Nov 2024 12:54:28 +0000 (04:54 -0800)
src/lib/pool.ts
src/lib/wallet.ts
src/lib/workers.ts [new file with mode: 0644]
src/lib/workers/ckdBip44.ts
src/lib/workers/ckdBlake2b.ts

index f37a5ba2870ba51b3af1ffc2ce728b1d1be4fce6..0c3f9fd6b7138d62945f6507419e88ed65d96b43 100644 (file)
@@ -34,16 +34,10 @@ export class Pool {
 
        constructor (url: string) {
                for (let i = navigator.hardwareConcurrency - 1; i > 0; i--) {
-                       let workerUrl
-                       if (globalThis.window == null) {
-                               workerUrl = url
-                       } else {
-                               workerUrl = new URL(url, import.meta.url)
-                       }
                        const thread = {
                                isBusy: false,
                                //@ts-expect-error
-                               worker: new Worker(workerUrl, { type: 'module', eval: true })
+                               worker: new Worker(url, { type: 'module', eval: true })
                        }
                        thread.worker.addEventListener('message', (message) => {
                                this.#report(thread, message.data ?? message)
index 3a4685157804cc6dcc68e8f232175e8aa82b3036..e0e553b8481709a059fdb7c4f36f4e9be31bcb5c 100644 (file)
@@ -1,14 +1,10 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>\r
 // SPDX-License-Identifier: GPL-3.0-or-later\r
 \r
-import blake2b from 'blake2b'\r
+import { ckdBip44, ckdBlake2b } from './workers.js'\r
 import { Account } from './account.js'\r
-import { nanoCKD } from './bip32-key-derivation.js'\r
 import { Bip39Mnemonic } from './bip39-mnemonic.js'\r
-import { ckdBip44Worker } from './workers/ckdBip44.js'\r
-import { ckdBlake2bWorker } from './workers/ckdBlake2b.js'\r
 import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js'\r
-import { dec, hex } from './convert.js'\r
 import { Entropy } from './entropy.js'\r
 import { Pool } from './pool.js'\r
 import { Rpc } from './rpc.js'\r
@@ -267,13 +263,7 @@ export class Bip44Wallet extends Wallet {
                        throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)\r
                }\r
                super(seed, mnemonic, id)\r
-               if (globalThis.Window) {\r
-                       const ckdBlob = new Blob([ckdBip44Worker], { type: 'application/javascript' })\r
-                       const ckdUrl = URL.createObjectURL(ckdBlob)\r
-                       this.#pool = new Pool(ckdUrl)\r
-               } else {\r
-                       this.#pool = new Pool(ckdBip44Worker)\r
-               }\r
+               this.#pool = new Pool(ckdBip44)\r
                Bip44Wallet.#isInternal = false\r
        }\r
 \r
@@ -466,13 +456,7 @@ export class Blake2bWallet extends Wallet {
                        throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)\r
                }\r
                super(seed, mnemonic, id)\r
-               if (globalThis.Window) {\r
-                       const ckdBlob = new Blob([ckdBlake2bWorker], { type: 'application/javascript' })\r
-                       const ckdUrl = URL.createObjectURL(ckdBlob)\r
-                       this.#pool = new Pool(ckdUrl)\r
-               } else {\r
-                       this.#pool = new Pool(ckdBlake2bWorker)\r
-               }\r
+               this.#pool = new Pool(ckdBlake2b)\r
                Blake2bWallet.#isInternal = false\r
        }\r
 \r
diff --git a/src/lib/workers.ts b/src/lib/workers.ts
new file mode 100644 (file)
index 0000000..8828c7d
--- /dev/null
@@ -0,0 +1,5 @@
+import ckdBip44 from './workers/ckdBip44.js'
+import ckdBlake2b from './workers/ckdBlake2b.js'
+export { ckdBip44, ckdBlake2b }
+// import './workers/nano-nacl.js'
+// import './workers/passkey.js'
index 42216eb7dc9ecb648618b1345fd2816555f75e9e..56a48c72a2793281b204ba0f56a674aa6b51a908 100644 (file)
@@ -6,102 +6,100 @@ type ExtendedKey = {
        chainCode: DataView
 }
 
-async function ckdBip44 () {
-       const BIP44_COIN_NANO = 165
-       const BIP44_PURPOSE = 44
-       const HARDENED_OFFSET = 0x80000000
-       const SLIP10_ED25519 = 'ed25519 seed'
-       let addEventListener = globalThis.addEventListener
-       let postMessage = globalThis.postMessage
-       if (addEventListener == null || postMessage == null) {
-               const { isMainThread, parentPort } = await import('node:worker_threads')
-               if (!isMainThread && parentPort) {
-                       addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort)
-                       postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
-               }
+const BIP44_COIN_NANO = 165
+const BIP44_PURPOSE = 44
+const HARDENED_OFFSET = 0x80000000
+const SLIP10_ED25519 = 'ed25519 seed'
+let addEventListener = globalThis.addEventListener
+let postMessage = globalThis.postMessage
+if (addEventListener == null || postMessage == null) {
+       const { isMainThread, parentPort } = await import('node:worker_threads')
+       if (!isMainThread && parentPort) {
+               addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort)
+               postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
        }
+}
 
-       /**
-       * Listens for messages from a calling function.
-       */
-       addEventListener('message', (message) => {
-               const { seed, index } = message.data ?? message
-               nanoCKD(seed, index).then(key => postMessage({ index, key }))
-       })
+/**
+* Listens for messages from a calling function.
+*/
+addEventListener('message', (message) => {
+       const { seed, index } = message.data ?? message
+       nanoCKD(seed, index).then(key => postMessage({ index, key }))
+})
 
-       /**
-       * Derives a private child key following the BIP-32 and BIP-44 derivation path
-       * registered to the Nano block lattice. Only hardened child keys are defined.
-       *
-       * @param {string} seed - Hexadecimal seed derived from mnemonic phrase
-       * @param {number} index - Account number between 0 and 2^31-1
-       * @returns Private child key for the account
-       */
-       async function nanoCKD (seed: string, index: number): Promise<string> {
-               if (seed.length < 32 || seed.length > 128) {
-                       throw new RangeError(`Invalid seed length`)
-               }
-               if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) {
-                       throw new RangeError(`Invalid child key index 0x${index.toString(16)}`)
-               }
-               const masterKey = await slip10(SLIP10_ED25519, seed)
-               const purposeKey = await CKDpriv(masterKey, BIP44_PURPOSE + HARDENED_OFFSET)
-               const coinKey = await CKDpriv(purposeKey, BIP44_COIN_NANO + HARDENED_OFFSET)
-               const accountKey = await CKDpriv(coinKey, index + HARDENED_OFFSET)
-               const privateKey = new Uint8Array(accountKey.privateKey.buffer)
-               return privateKey.reduce((key, byte) => key.concat(byte.toString(16).padStart(2, '0')), '')
+/**
+* Derives a private child key following the BIP-32 and BIP-44 derivation path
+* registered to the Nano block lattice. Only hardened child keys are defined.
+*
+* @param {string} seed - Hexadecimal seed derived from mnemonic phrase
+* @param {number} index - Account number between 0 and 2^31-1
+* @returns Private child key for the account
+*/
+async function nanoCKD (seed: string, index: number): Promise<string> {
+       if (seed.length < 32 || seed.length > 128) {
+               throw new RangeError(`Invalid seed length`)
        }
-
-       async function slip10 (curve: string, S: string): Promise<ExtendedKey> {
-               const key = new TextEncoder().encode(curve)
-               const data = new Uint8Array(64)
-               data.set(S.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)))
-               const I = await hmac(key, data)
-               const IL = new DataView(I.buffer.slice(0, I.length / 2))
-               const IR = new DataView(I.buffer.slice(I.length / 2))
-               return ({ privateKey: IL, chainCode: IR })
+       if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) {
+               throw new RangeError(`Invalid child key index 0x${index.toString(16)}`)
        }
+       const masterKey = await slip10(SLIP10_ED25519, seed)
+       const purposeKey = await CKDpriv(masterKey, BIP44_PURPOSE + HARDENED_OFFSET)
+       const coinKey = await CKDpriv(purposeKey, BIP44_COIN_NANO + HARDENED_OFFSET)
+       const accountKey = await CKDpriv(coinKey, index + HARDENED_OFFSET)
+       const privateKey = new Uint8Array(accountKey.privateKey.buffer)
+       return privateKey.reduce((key, byte) => key.concat(byte.toString(16).padStart(2, '0')), '')
+}
 
-       async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise<ExtendedKey> {
-               const key = new Uint8Array(chainCode.buffer)
-               const data = new Uint8Array(37)
-               data.set([0])
-               data.set(ser256(privateKey), 1)
-               data.set(ser32(index), 33)
-               const I = await hmac(key, data)
-               const IL = new DataView(I.buffer.slice(0, I.length / 2))
-               const IR = new DataView(I.buffer.slice(I.length / 2))
-               return ({ privateKey: IL, chainCode: IR })
-       }
+async function slip10 (curve: string, S: string): Promise<ExtendedKey> {
+       const key = new TextEncoder().encode(curve)
+       const data = new Uint8Array(64)
+       data.set(S.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)))
+       const I = await hmac(key, data)
+       const IL = new DataView(I.buffer.slice(0, I.length / 2))
+       const IR = new DataView(I.buffer.slice(I.length / 2))
+       return ({ privateKey: IL, chainCode: IR })
+}
 
-       function ser32 (integer: number): Uint8Array {
-               if (typeof integer !== 'number') {
-                       throw new TypeError(`Expected a number, received ${typeof integer}`)
-               }
-               if (integer > 0xffffffff) {
-                       throw new RangeError(`Expected 32-bit integer, received ${integer.toString(2).length}-bit value: ${integer}`)
-               }
-               const view = new DataView(new ArrayBuffer(4))
-               view.setUint32(0, integer, false)
-               return new Uint8Array(view.buffer)
-       }
+async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise<ExtendedKey> {
+       const key = new Uint8Array(chainCode.buffer)
+       const data = new Uint8Array(37)
+       data.set([0])
+       data.set(ser256(privateKey), 1)
+       data.set(ser32(index), 33)
+       const I = await hmac(key, data)
+       const IL = new DataView(I.buffer.slice(0, I.length / 2))
+       const IR = new DataView(I.buffer.slice(I.length / 2))
+       return ({ privateKey: IL, chainCode: IR })
+}
 
-       function ser256 (integer: DataView): Uint8Array {
-               if (integer.constructor !== DataView) {
-                       throw new TypeError(`Expected DataView, received ${typeof integer}`)
-               }
-               if (integer.byteLength > 32) {
-                       throw new RangeError(`Expected 32-byte integer, received ${integer.byteLength}-byte value: ${integer}`)
-               }
-               return new Uint8Array(integer.buffer)
+function ser32 (integer: number): Uint8Array {
+       if (typeof integer !== 'number') {
+               throw new TypeError(`Expected a number, received ${typeof integer}`)
+       }
+       if (integer > 0xffffffff) {
+               throw new RangeError(`Expected 32-bit integer, received ${integer.toString(2).length}-bit value: ${integer}`)
        }
+       const view = new DataView(new ArrayBuffer(4))
+       view.setUint32(0, integer, false)
+       return new Uint8Array(view.buffer)
+}
 
-       async function hmac (key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
-               const { subtle } = globalThis.crypto
-               const pk = await subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign'])
-               const signature = await subtle.sign('HMAC', pk, data)
-               return new Uint8Array(signature)
+function ser256 (integer: DataView): Uint8Array {
+       if (integer.constructor !== DataView) {
+               throw new TypeError(`Expected DataView, received ${typeof integer}`)
        }
+       if (integer.byteLength > 32) {
+               throw new RangeError(`Expected 32-byte integer, received ${integer.byteLength}-byte value: ${integer}`)
+       }
+       return new Uint8Array(integer.buffer)
+}
+
+async function hmac (key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
+       const { subtle } = globalThis.crypto
+       const pk = await subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign'])
+       const signature = await subtle.sign('HMAC', pk, data)
+       return new Uint8Array(signature)
 }
 
-export const ckdBip44Worker = `(${ckdBip44.toString()})()`
+export default import.meta.url
index 55f974320532dba59e476598dc980c9b7adc81fe..10a477da8827b891d315149ebb9f8e412438b438 100644 (file)
@@ -3,42 +3,37 @@
 
 import blake2b from 'blake2b'
 
-async function ckdBlake2b () {
-       let blake2b: any
-
-       let addEventListener = globalThis.addEventListener
-       let postMessage = globalThis.postMessage
-       if (addEventListener == null || postMessage == null) {
-               blake2b = (await import('blake2b')).default
-               const { isMainThread, parentPort } = await import('node:worker_threads')
-               if (!isMainThread && parentPort) {
-                       addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort)
-                       postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
-               }
+let addEventListener = globalThis.addEventListener
+let postMessage = globalThis.postMessage
+if (addEventListener == null || postMessage == null) {
+       const { isMainThread, parentPort } = await import('node:worker_threads')
+       if (!isMainThread && parentPort) {
+               addEventListener = Object.getPrototypeOf(parentPort).addListener.bind(parentPort)
+               postMessage = Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
        }
+}
 
-       /**
-       * Listens for messages from a calling function.
-       */
-       addEventListener('message', (message) => {
-               const { seed, index } = message.data ?? message
-               ckdBlake2b(seed, index).then(key => postMessage({ index, key }))
-       })
+/**
+* Listens for messages from a calling function.
+*/
+addEventListener('message', (message) => {
+       const { seed, index } = message.data ?? message
+       ckdBlake2b(seed, index).then(key => postMessage({ index, key }))
+})
 
-       /**
-       * Derives BLAKE2b account private keys.
-       *
-       * @param {number} index - Index of the account
-       * @returns {Promise<string>}
-       */
-       async function ckdBlake2b (seed: string, index: number): Promise<string> {
-               const indexHex = index.toString(16).padStart(8, '0').toUpperCase()
-               const inputHex = `${seed}${indexHex}`.padStart(72, '0')
-               const inputArray = (inputHex.match(/.{1,2}/g) ?? []).map(h => parseInt(h, 16))
-               const inputBytes = Uint8Array.from(inputArray)
-               const hash = blake2b(32).update(inputBytes).digest('hex')
-               return hash
-       }
+/**
+* Derives BLAKE2b account private keys.
+*
+* @param {number} index - Index of the account
+* @returns {Promise<string>}
+*/
+async function ckdBlake2b (seed: string, index: number): Promise<string> {
+       const indexHex = index.toString(16).padStart(8, '0').toUpperCase()
+       const inputHex = `${seed}${indexHex}`.padStart(72, '0')
+       const inputArray = (inputHex.match(/.{1,2}/g) ?? []).map(h => parseInt(h, 16))
+       const inputBytes = Uint8Array.from(inputArray)
+       const hash = blake2b(32).update(inputBytes).digest('hex')
+       return hash
 }
 
-export const ckdBlake2bWorker = `(${ckdBlake2b.toString()})()`
+export default import.meta.url