]> git.codecow.com Git - libnemo.git/commitdiff
Declassify Tools class into module functions.
authorChris Duncan <chris@codecow.com>
Sun, 5 Jul 2026 19:39:50 +0000 (12:39 -0700)
committerChris Duncan <chris@codecow.com>
Sun, 5 Jul 2026 19:39:50 +0000 (12:39 -0700)
src/index.ts
src/lib/block/index.ts
src/lib/block/receive.ts
src/lib/block/send.ts
src/lib/rolodex.ts
src/lib/tools.ts

index 9b95e52e1e112b9b213b2e482c650dcf77211591..bbc9941cb856d89d32f3e4c7c8d8d99425110aca 100644 (file)
@@ -7,7 +7,7 @@ import { Blake2b } from './lib/crypto'
 import { Ledger } from './lib/ledger'
 import { Rolodex } from './lib/rolodex'
 import { Rpc } from './lib/rpc'
-import { Tools } from './lib/tools'
+import * as Tools from './lib/tools'
 import { Wallet } from './lib/wallet'
 
 declare global {
index c76dc8693cdc2592fff096705ef151ba3e512c12..71b53f1ac94465ac0bdc899a3c17316f1150d4ce 100644 (file)
@@ -7,7 +7,7 @@ import { DIFFICULTY_RECEIVE, DIFFICULTY_SEND, PREAMBLE } from '../constants'
 import { bytes, dec, hex } from '../convert'
 import { Blake2b } from '../crypto'
 import { Rpc } from '../rpc'
-import { Tools } from '../tools'
+import { convert } from '../tools'
 import { Wallet } from '../wallet'
 import { _change } from './change'
 import { _receive } from './receive'
@@ -75,7 +75,7 @@ export class Block {
                                throw new TypeError('Account frontier is unknown')
                        }
                        this.account = account
-                       this.balance = Tools.convert(balance, 'raw', 'raw', 'bigint')
+                       this.balance = convert(balance, 'raw', 'raw', 'bigint')
                        this.previous = hex.toBytes(previous, 32)
                        if (representative instanceof Account) {
                                this.representative = representative
index 7d823737fceab7b37168a22d84ee55a552ef2c33..bfb5229d21b398b036b95cc13ca39d25dcbd1e80 100644 (file)
@@ -4,7 +4,7 @@
 import type { Block } from '.'
 import { UNITS } from '../constants'
 import { hex } from '../convert'
-import { Tools } from '../tools'
+import { convert } from '../tools'
 
 /**
  * Set the amount of nano that this block will receive from a corresponding
@@ -31,7 +31,7 @@ export function _receive (block: Block, sendBlock: unknown, amount: unknown, uni
                if (typeof amount !== 'bigint' && typeof amount !== 'number' && typeof amount !== 'string') {
                        throw new TypeError('Invalid amount')
                }
-               block.balance += Tools.convert(amount, unit, 'raw', 'bigint')
+               block.balance += convert(amount, unit, 'raw', 'bigint')
 
                if (typeof sendBlock !== 'string' && !(sendBlock instanceof (block.constructor as typeof Block))) {
                        throw new TypeError('Invalid send block')
index e49df01c268c91765c71c7b2774e36e70587990b..00552af2a2d329b37217df5ea2a78f6da0714bde 100644 (file)
@@ -5,7 +5,7 @@ import type { Block } from '.'
 import { Account } from '../account'
 import { UNITS } from '../constants'
 import { hex } from '../convert'
-import { Tools } from '../tools'
+import { convert } from '../tools'
 /**
  * Set the amount of nano that this block will send to a recipient account.
  *
@@ -30,7 +30,7 @@ export function _send (block: Block, account: unknown, amount: unknown, unit: un
                if (typeof amount !== 'bigint' && typeof amount !== 'number' && typeof amount !== 'string') {
                        throw new TypeError(`Invalid amount ${amount}`, { cause: typeof amount })
                }
-               block.balance -= Tools.convert(amount, unit, 'raw', 'bigint')
+               block.balance -= convert(amount, unit, 'raw', 'bigint')
 
                if (block.balance < 0) {
                        throw new RangeError('Insufficient funds', { cause: block.balance })
index 147f630bfc73b2bf417f832c8d1be99072b748f1..02dd42296297b90ac3de7e9424e95ad54b4cc1b5 100644 (file)
@@ -3,7 +3,7 @@
 
 import { Account } from './account'
 import { Database } from './database'
-import { Tools } from './tools'
+import * as Tools from './tools'
 
 /**
 * Represents a basic address book of Nano accounts. Multiple addresses can be
index b48d7226d717e9d55a6f9962cb92568e3dfd1c3c..b1df95f948532445fa6fa9fd50c8ff1d08c1a794 100644 (file)
@@ -15,208 +15,220 @@ type SweepResult = {
        message: string
 }
 
-const decoder = Object.freeze(new TextDecoder())
-const encoder = Object.freeze(new TextEncoder())
-
+/**
+ * Decodes bytes to a UTF-8 string.
+ *
+ * @param {AllowSharedBufferSource} [input]
+ * @param {TextDecodeOptions} [options]
+ * @returns {string} Decoded string
+ */
 export function decode (input?: AllowSharedBufferSource, options?: TextDecodeOptions): string {
        return decoder.decode(input, options)
 }
 
+/**
+ * Encodes a UTF-8 string to bytes.
+ *
+ * @param {string} [input]
+ * @returns {Bytes} Encoded bytes
+ */
 export function encode (input?: string): Bytes {
        return encoder.encode(input)
 }
 
-export class Tools {
-       static #normalize (input: string | ArrayBuffer | Bytes): Bytes {
-               return (typeof input === 'string')
-                       ? hex.toBytes(input)
-                       : input instanceof ArrayBuffer
-                               ? new Uint8Array(input.slice())
-                               : input
-       }
-       /**
-        * Converts a decimal amount of nano from one unit divider to another.
-        *
-        * @param {(bigint|number|string)} amount - Decimal amount to convert
-        * @param {string} inputUnit - Current denomination
-        * @param {string} outputUnit - Desired denomination
-        * @param {string} [format] - Data type of output
-        */
-       static convert (amount: bigint | number | string, inputUnit: string, outputUnit: string): string
-       static convert (amount: bigint | number | string, inputUnit: string, outputUnit: string, format: 'bigint'): bigint
-       static convert (amount: bigint | number | string, inputUnit: string, outputUnit: string, format: 'number'): number
-       static convert (amount: bigint | number | string, inputUnit: string, outputUnit: string, format: 'string'): string
-       static convert (amount: unknown, inputUnit: unknown, outputUnit: unknown, format?: unknown): bigint | number | string {
-               if (typeof amount !== 'bigint' && typeof amount !== 'number' && typeof amount !== 'string') {
-                       throw new Error('Invalid amount', { cause: typeof amount })
-               }
-               if (typeof amount === 'string' && !/^[0-9]+\.?[0-9]*$/.test(amount)) {
-                       throw new Error('Invalid amount', { cause: amount })
-               }
-               if (typeof inputUnit !== 'string') {
-                       throw new TypeError('Invalid input unit', { cause: typeof inputUnit })
-               }
-               (inputUnit as string) = inputUnit.toUpperCase()
-               if (typeof outputUnit !== 'string') {
-                       throw new TypeError('Invalid output unit', { cause: typeof outputUnit })
-               }
-               (outputUnit as string) = outputUnit.toUpperCase()
-               if (UNITS[inputUnit] == null) {
-                       throw new Error(`Unknown denomination ${inputUnit}, expected one of the following: ${Object.keys(UNITS)}`)
-               }
-               if (UNITS[outputUnit] == null) {
-                       throw new Error(`Unknown denomination ${outputUnit}, expected one of the following: ${Object.keys(UNITS)}`)
-               }
-               if (format !== undefined && format !== 'bigint' && format !== 'number' && format !== 'string') {
-                       throw new Error('Invalid output format', { cause: format })
-               }
+/**
+ * Converts a decimal amount of nano from one unit divider to another.
+ *
+ * @param {(bigint|number|string)} amount - Decimal amount to convert
+ * @param {string} inputUnit - Current denomination
+ * @param {string} outputUnit - Desired denomination
+ * @param {string} [format] - Data type of output
+ */
+export function convert (amount: bigint | number | string, inputUnit: string, outputUnit: string): string
+export function convert (amount: bigint | number | string, inputUnit: string, outputUnit: string, format: 'bigint'): bigint
+export function convert (amount: bigint | number | string, inputUnit: string, outputUnit: string, format: 'number'): number
+export function convert (amount: bigint | number | string, inputUnit: string, outputUnit: string, format: 'string'): string
+export function convert (amount: unknown, inputUnit: unknown, outputUnit: unknown, format?: unknown): bigint | number | string {
+       if (typeof amount !== 'bigint' && typeof amount !== 'number' && typeof amount !== 'string') {
+               throw new Error('Invalid amount', { cause: typeof amount })
+       }
+       if (typeof amount === 'string' && !/^[0-9]+\.?[0-9]*$/.test(amount)) {
+               throw new Error('Invalid amount', { cause: amount })
+       }
+       if (typeof inputUnit !== 'string') {
+               throw new TypeError('Invalid input unit', { cause: typeof inputUnit })
+       }
+       (inputUnit as string) = inputUnit.toUpperCase()
+       if (typeof outputUnit !== 'string') {
+               throw new TypeError('Invalid output unit', { cause: typeof outputUnit })
+       }
+       (outputUnit as string) = outputUnit.toUpperCase()
+       if (UNITS[inputUnit] == null) {
+               throw new Error(`Unknown denomination ${inputUnit}, expected one of the following: ${Object.keys(UNITS)}`)
+       }
+       if (UNITS[outputUnit] == null) {
+               throw new Error(`Unknown denomination ${outputUnit}, expected one of the following: ${Object.keys(UNITS)}`)
+       }
+       if (format !== undefined && format !== 'bigint' && format !== 'number' && format !== 'string') {
+               throw new Error('Invalid output format', { cause: format })
+       }
 
-               const inUnit = UNITS[inputUnit]
-               let [i, f] = typeof amount === 'string'
-                       ? amount.split('.')
-                       : dec.toString(amount).split('.')
-               i = i.replace(/^0*/g, '')
-               f = f?.replace(/0*$/g, '')
-               if (f?.length > inUnit) {
-                       throw new RangeError('Amount contains fractional raw')
-               }
+       const inUnit = UNITS[inputUnit]
+       let [i, f] = typeof amount === 'string'
+               ? amount.split('.')
+               : dec.toString(amount).split('.')
+       i = i.replace(/^0*/g, '')
+       f = f?.replace(/0*$/g, '')
+       if (f?.length > inUnit) {
+               throw new RangeError('Amount contains fractional raw')
+       }
 
-               // convert to raw
-               let shift = 0
-               while (shift++ < inUnit) i += '0'
-               let int = BigInt(i || '0')
-               if (f != null) {
-                       shift = f.length
-                       while (shift++ < inUnit) f += '0'
-                       int += BigInt(f || '0')
-               }
-               if (int > MAX_SUPPLY) {
-                       throw new Error('Amount exceeds available supply')
+       // convert to raw
+       let shift = 0
+       while (shift++ < inUnit) i += '0'
+       let int = BigInt(i || '0')
+       if (f != null) {
+               shift = f.length
+               while (shift++ < inUnit) f += '0'
+               int += BigInt(f || '0')
+       }
+       if (int > MAX_SUPPLY) {
+               throw new Error('Amount exceeds available supply')
+       }
+       if (int < 0n) {
+               throw new Error('Amount must be non-negative')
+       }
+
+       // convert to desired denomination
+       const outUnit = UNITS[outputUnit]
+       i = dec.toString(int, 40)
+       f = i.slice(40 - outUnit).replace(/0*$/g, '')
+       i = i.slice(0, 40 - outUnit).replace(/^0*/g, '')
+       const output = `${i === '' ? '0' : i}${f === '' ? '' : '.'}${f}`
+
+       switch (format) {
+               case 'bigint': {
+                       if (!f) return BigInt(output)
+                       throw new RangeError('Output fractional amount truncated')
                }
-               if (int < 0n) {
-                       throw new Error('Amount must be non-negative')
+               case 'number': {
+                       if (Number(i) <= Number.MAX_SAFE_INTEGER) return Number(output)
+                       throw new RangeError('Output larger than Number.MAX_SAFE_INTEGER')
                }
+               case 'string':
+               default: return output
+       }
+}
 
-               // convert to desired denomination
-               const outUnit = UNITS[outputUnit]
-               i = dec.toString(int, 40)
-               f = i.slice(40 - outUnit).replace(/0*$/g, '')
-               i = i.slice(0, 40 - outUnit).replace(/^0*/g, '')
-               const output = `${i === '' ? '0' : i}${f === '' ? '' : '.'}${f}`
-
-               switch (format) {
-                       case 'bigint': {
-                               if (!f) return BigInt(output)
-                               throw new RangeError('Output fractional amount truncated')
-                       }
-                       case 'number': {
-                               if (Number(i) <= Number.MAX_SAFE_INTEGER) return Number(output)
-                               throw new RangeError('Output larger than Number.MAX_SAFE_INTEGER')
-                       }
-                       case 'string':
-                       default: return output
-               }
+/**
+ * Signs an arbitrary string with a secret key using nano25519. The input data
+ * is encoded as UTF-8 and can be up to 32 KiB in total.
+ *
+ * @param {(string|ArrayBuffer|Bytes)} secretKey - 64-byte secret key
+ * @param {string} input - Data to be signed
+ * @returns {string} 64-byte hexadecimal signature
+ */
+export function sign (secretKey: string | ArrayBuffer | Bytes, input: string): string {
+       if (navigator.userActivation?.isActive === false) {
+               throw new DOMException(
+                       'Signing request was blocked due to lack of user activation',
+                       'NotAllowedError'
+               )
+       }
+       const k = normalize(secretKey)
+       try {
+               const signature = nano25519_sign(utf8.toBytes(input), k)
+               return bytes.toHex(signature)
+       } catch (err) {
+               throw new Error(`Failed to sign message`, { cause: err })
+       } finally {
+               k.fill(0)
        }
+}
 
-       /**
-        * Signs an arbitrary string with a secret key using nano25519. The input data
-        * is encoded as UTF-8 and can be up to 32 KiB in total.
-        *
-        * @param {(string|ArrayBuffer|Bytes)} secretKey - 64-byte secret key
-        * @param {string} input - Data to be signed
-        * @returns {string} 64-byte hexadecimal signature
-        */
-       static sign (secretKey: string | ArrayBuffer | Bytes, input: string): string {
-               if (navigator.userActivation?.isActive === false) {
-                       throw new DOMException(
-                               'Signing request was blocked due to lack of user activation',
-                               'NotAllowedError'
-                       )
-               }
-               const k = this.#normalize(secretKey)
-               try {
-                       const signature = nano25519_sign(utf8.toBytes(input), k)
-                       return bytes.toHex(signature)
-               } catch (err) {
-                       throw new Error(`Failed to sign message`, { cause: err })
-               } finally {
-                       k.fill(0)
-               }
+/**
+ * Collects the funds from a specified range of accounts in a wallet and sends
+ * them all to a single recipient address. Hardware wallets are unsupported.
+ *
+ * @param {(Rpc|string|URL)} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks
+ * @param {Wallet} wallet - Wallet from which to sweep funds
+ * @param {string} recipient - Destination address for all swept funds
+ * @param {number} [from=0] - Starting account index to sweep
+ * @param {number} [to=from] - Ending account index to sweep
+ * @returns An array of results including both successes and failures
+ */
+export async function sweep (
+       rpc: Rpc | string | URL,
+       wallet: Wallet,
+       recipient: string,
+       from: number = 0,
+       to: number = from
+): Promise<SweepResult[]> {
+       if (rpc == null || wallet == null || recipient == null) {
+               throw new ReferenceError('Missing required sweep arguments')
+       }
+       if (typeof rpc === 'string' || rpc instanceof URL) {
+               rpc = new Rpc(rpc)
+       }
+       if (!(rpc instanceof Rpc)) {
+               throw new TypeError('RPC must be a valid node')
        }
 
-       /**
-        * Collects the funds from a specified range of accounts in a wallet and sends
-        * them all to a single recipient address. Hardware wallets are unsupported.
-        *
-        * @param {(Rpc|string|URL)} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks
-        * @param {Wallet} wallet - Wallet from which to sweep funds
-        * @param {string} recipient - Destination address for all swept funds
-        * @param {number} [from=0] - Starting account index to sweep
-        * @param {number} [to=from] - Ending account index to sweep
-        * @returns An array of results including both successes and failures
-        */
-       static async sweep (
-               rpc: Rpc | string | URL,
-               wallet: Wallet,
-               recipient: string,
-               from: number = 0,
-               to: number = from
-       ): Promise<SweepResult[]> {
-               if (rpc == null || wallet == null || recipient == null) {
-                       throw new ReferenceError('Missing required sweep arguments')
-               }
-               if (typeof rpc === 'string' || rpc instanceof URL) {
-                       rpc = new Rpc(rpc)
-               }
-               if (!(rpc instanceof Rpc)) {
-                       throw new TypeError('RPC must be a valid node')
-               }
+       const blockQueue: Promise<void>[] = []
+       const results: SweepResult[] = []
+       const recipientAccount = new Account(recipient)
+       const accounts = await wallet.refresh(rpc, from, to)
 
-               const blockQueue: Promise<void>[] = []
-               const results: SweepResult[] = []
-               const recipientAccount = new Account(recipient)
-               const accounts = await wallet.refresh(rpc, from, to)
-
-               for (const [index, account] of accounts) {
-                       const blockRequest: Promise<void> = new Promise(async (resolve) => {
-                               let block
-                               try {
-                                       if (index == null) {
-                                               throw new TypeError('Account index is required', { cause: account })
-                                       }
-                                       block = await new Block(account)
-                                               .send(recipientAccount, account.balance ?? 0n)
-                                               .sign(wallet, index)
-                                       await block.pow()
-                                       const hash = await block.process(rpc)
-                                       results.push({ status: 'success', address: block.account.address, message: hash })
-                               } catch (err: any) {
-                                       results.push({ status: 'error', address: account.address, message: err.message })
-                               } finally {
-                                       resolve()
+       for (const [index, account] of accounts) {
+               const blockRequest: Promise<void> = new Promise(async (resolve) => {
+                       let block
+                       try {
+                               if (index == null) {
+                                       throw new TypeError('Account index is required', { cause: account })
                                }
-                       })
-                       blockQueue.push(blockRequest)
-               }
-               await Promise.allSettled(blockQueue)
-               return results
-       }
-
-       /**
-        * Verifies the signature of an arbitrary string using a public key.
-        *
-        * @param {(string|ArrayBuffer|Bytes)} publicKey - 32-byte hexadecimal public key
-        * @param {(string|ArrayBuffer|Bytes)} signature - 128-character hexadcimal signature
-        * @param {string} input - Data to be verified
-        * @returns {boolean} True if the data was signed by the public key's matching private key
-        */
-       static verify (publicKey: string | ArrayBuffer | Bytes, signature: string | ArrayBuffer | Bytes, input: string): boolean {
-               const k = this.#normalize(publicKey)
-               const s = this.#normalize(signature)
-               try {
-                       return nano25519_verify(s, utf8.toBytes(input), k)
-               } catch (err) {
-                       throw new Error('Failed to verify signature', { cause: err })
-               }
+                               block = await new Block(account)
+                                       .send(recipientAccount, account.balance ?? 0n)
+                                       .sign(wallet, index)
+                               await block.pow()
+                               const hash = await block.process(rpc)
+                               results.push({ status: 'success', address: block.account.address, message: hash })
+                       } catch (err: any) {
+                               results.push({ status: 'error', address: account.address, message: err.message })
+                       } finally {
+                               resolve()
+                       }
+               })
+               blockQueue.push(blockRequest)
+       }
+       await Promise.allSettled(blockQueue)
+       return results
+}
+
+/**
+ * Verifies the signature of an arbitrary string using a public key.
+ *
+ * @param {(string|ArrayBuffer|Bytes)} publicKey - 32-byte hexadecimal public key
+ * @param {(string|ArrayBuffer|Bytes)} signature - 128-character hexadcimal 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 function verify (publicKey: string | ArrayBuffer | Bytes, signature: string | ArrayBuffer | Bytes, input: string): boolean {
+       const k = normalize(publicKey)
+       const s = normalize(signature)
+       try {
+               return nano25519_verify(s, utf8.toBytes(input), k)
+       } catch (err) {
+               throw new Error('Failed to verify signature', { cause: err })
        }
 }
+
+function normalize (input: string | ArrayBuffer | Bytes): Bytes {
+       return (typeof input === 'string')
+               ? hex.toBytes(input)
+               : input instanceof ArrayBuffer
+                       ? new Uint8Array(input.slice())
+                       : input
+}
+
+const decoder = Object.freeze(new TextDecoder())
+const encoder = Object.freeze(new TextEncoder())