]> zoso.dev Git - libnemo.git/commitdiff
Save work on testing browser compatibility.
authorChris Duncan <chris@zoso.dev>
Thu, 21 Nov 2024 23:10:35 +0000 (15:10 -0800)
committerChris Duncan <chris@zoso.dev>
Thu, 21 Nov 2024 23:10:35 +0000 (15:10 -0800)
src/lib/pool.ts
src/lib/wallet.ts
src/lib/workers/ckdBip44.ts
src/lib/workers/ckdBlake2b.ts
test.html [new file with mode: 0644]

index b153485639335e1c445bb26bcc428c015d85f12c..f37a5ba2870ba51b3af1ffc2ce728b1d1be4fce6 100644 (file)
@@ -32,11 +32,18 @@ export class Pool {
                return true
        }
 
-       constructor (url: string | URL) {
+       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,
-                               worker: new Worker(new URL(url, import.meta.url), { type: 'module' })
+                               //@ts-expect-error
+                               worker: new Worker(workerUrl, { type: 'module', eval: true })
                        }
                        thread.worker.addEventListener('message', (message) => {
                                this.#report(thread, message.data ?? message)
index a1b74cb1a6c95a5ddf21190849d7dfa061294f7a..3a4685157804cc6dcc68e8f232175e8aa82b3036 100644 (file)
@@ -5,6 +5,8 @@ import blake2b from 'blake2b'
 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
@@ -265,7 +267,13 @@ 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
-               this.#pool = new Pool('./workers/ckdBip44.js')\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
                Bip44Wallet.#isInternal = false\r
        }\r
 \r
@@ -458,7 +466,13 @@ 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
-               this.#pool = new Pool('./workers/ckdBlake2b.js')\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
                Blake2bWallet.#isInternal = false\r
        }\r
 \r
index 369742f98cf7cf1188af1032260c4710bb553a42..2f889ef42c08521b50835b0b17c44335d3dd5585 100644 (file)
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
-import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } from '../constants.js'
-import { bytes, dec, hex, utf8 } from '../convert.js'
 
 type ExtendedKey = {
        privateKey: string
        chainCode: string
 }
 
-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)
+async function ckdBip44 () {
+       let BIP44_COIN_NANO: any, BIP44_PURPOSE: any, HARDENED_OFFSET: any, SLIP10_ED25519: any
+       let bytes: any, dec: any, hex: any, utf8: any
+       let addEventListener = globalThis.addEventListener
+       let postMessage = globalThis.postMessage
+       if (addEventListener == null || postMessage == null) {
+               ({ BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } = await import(new URL('dist/lib/constants.js', import.meta.url).toString()));
+               ({ bytes, dec, hex, utf8 } = await import(new URL('./dist/lib/convert.js', import.meta.url).toString()))
+               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)
+               }
+       } else {
+               ({ BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } = await import('../constants.js'));
+               ({ bytes, dec, hex, utf8 } = await import('../convert.js'))
        }
-}
 
-/**
-* 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
-*/
-export async function nanoCKD (seed: string, index: number): Promise<string> {
-       if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) {
-               throw new RangeError(`Invalid child key index 0x${index.toString(16)}`)
+       /**
+       * 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 (!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)
+               return accountKey.privateKey
        }
-       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)
-       return accountKey.privateKey
-}
-
-async function slip10 (curve: string, S: string): Promise<ExtendedKey> {
-       const key = utf8.toBytes(curve)
-       const data = hex.toBytes(S)
-       const I = await hmac(key, data)
-       const IL = I.slice(0, I.length / 2)
-       const IR = I.slice(I.length / 2)
-       return ({ privateKey: IL, chainCode: IR })
-}
 
-async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise<ExtendedKey> {
-       const key = hex.toBytes(chainCode)
-       const data = hex.toBytes(`00${bytes.toHex(ser256(privateKey))}${bytes.toHex(ser32(index))}`)
-       const I = await hmac(key, data)
-       const IL = I.slice(0, I.length / 2)
-       const IR = I.slice(I.length / 2)
-       return ({ privateKey: IL, chainCode: IR })
-}
+       async function slip10 (curve: string, S: string): Promise<ExtendedKey> {
+               const key = utf8.toBytes(curve)
+               const data = hex.toBytes(S)
+               const I = await hmac(key, data)
+               const IL = I.slice(0, I.length / 2)
+               const IR = I.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}`)
+       async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise<ExtendedKey> {
+               const key = hex.toBytes(chainCode)
+               const data = hex.toBytes(`00${bytes.toHex(ser256(privateKey))}${bytes.toHex(ser32(index))}`)
+               const I = await hmac(key, data)
+               const IL = I.slice(0, I.length / 2)
+               const IR = I.slice(I.length / 2)
+               return ({ privateKey: IL, chainCode: IR })
        }
-       const bits = dec.toBin(integer)
-       if (bits.length > 32) {
-               throw new RangeError(`Expected 32-bit integer, received ${bits.length}-bit value: ${integer}`)
+
+       function ser32 (integer: number): Uint8Array {
+               if (typeof integer !== 'number') {
+                       throw new TypeError(`Expected a number, received ${typeof integer}`)
+               }
+               const bits = dec.toBin(integer)
+               if (bits.length > 32) {
+                       throw new RangeError(`Expected 32-bit integer, received ${bits.length}-bit value: ${integer}`)
+               }
+               return dec.toBytes(integer, 4)
        }
-       return dec.toBytes(integer, 4)
-}
 
-function ser256 (integer: string): Uint8Array {
-       if (typeof integer !== 'string') {
-               throw new TypeError(`Expected string, received ${typeof integer}`)
+       function ser256 (integer: string): Uint8Array {
+               if (typeof integer !== 'string') {
+                       throw new TypeError(`Expected string, received ${typeof integer}`)
+               }
+               const bits = hex.toBin(integer)
+               if (bits.length > 256) {
+                       throw new RangeError(`Expected 256-bit integer, received ${bits.length}-bit value: ${integer}`)
+               }
+               return hex.toBytes(integer, 32)
        }
-       const bits = hex.toBin(integer)
-       if (bits.length > 256) {
-               throw new RangeError(`Expected 256-bit integer, received ${bits.length}-bit value: ${integer}`)
+
+       async function hmac (key: Uint8Array, data: Uint8Array): Promise<string> {
+               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 bytes.toHex(new Uint8Array(signature))
        }
-       return hex.toBytes(integer, 32)
 }
 
-async function hmac (key: Uint8Array, data: Uint8Array): Promise<string> {
-       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 bytes.toHex(new Uint8Array(signature))
-}
+const ckdBip44Worker = `(${ckdBip44.toString()})()`
+// const ckdBip44Worker = `data:text/javascript,(${ckdBip44.toString()})()`
+export { ckdBip44Worker }
+
index e807fd36bc262038c0856c6d5febc4edd05d295c..2995aeda82c598dc8a0e3b7d33e324d88ba84fbe 100644 (file)
@@ -1,37 +1,43 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import blake2b from 'blake2b'
+async function ckdBlake2b () {
+       const { default: blake2b } = await import('blake2b')
 
-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)
+       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
+       }
 }
+
+const ckdBlake2bWorker = `(${ckdBlake2b.toString()})()`
+// const ckdBlake2bWorker = `data:text/javascript,(${ckdBlake2b.toString()})()`
+export { ckdBlake2bWorker }
diff --git a/test.html b/test.html
new file mode 100644 (file)
index 0000000..f7cc454
--- /dev/null
+++ b/test.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+
+<head>
+       <script type="module" src="./dist/global.min.js"></script>
+       <style>body{background:black;}</style>
+</head>
+
+<body></body>
+
+</html>