]> git.codecow.com Git - libnemo.git/commitdiff
Extract common block functions to separate files.
authorChris Duncan <chris@codecow.com>
Sun, 28 Jun 2026 06:21:05 +0000 (23:21 -0700)
committerChris Duncan <chris@codecow.com>
Sun, 28 Jun 2026 06:21:05 +0000 (23:21 -0700)
src/lib/block/change.ts [new file with mode: 0644]
src/lib/block/index.ts
src/lib/block/receive.ts [new file with mode: 0644]
src/lib/block/send.ts [new file with mode: 0644]
src/lib/block/sign.ts [new file with mode: 0644]
src/lib/block/validate.ts [new file with mode: 0644]
src/lib/block/verify.ts [new file with mode: 0644]

diff --git a/src/lib/block/change.ts b/src/lib/block/change.ts
new file mode 100644 (file)
index 0000000..a8d7748
--- /dev/null
@@ -0,0 +1,33 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import type { Block } from '.'
+import { Account } from '../account'
+import { BURN_PUBLIC_KEY } from '../constants'
+import { hex } from '../convert'
+
+export function _change (block: Block, representative: unknown): Block {
+       const { link, representative: rep, subtype } = block
+       try {
+               if (block.subtype != null) {
+                       throw new Error(`Block already configured as ${block.subtype}`)
+               }
+               block.subtype = 'change'
+
+               if (typeof representative !== 'string' && !(representative instanceof Account)) {
+                       throw new TypeError('Invalid account')
+               }
+               block.representative = (typeof representative === 'string')
+                       ? new Account(representative)
+                       : representative
+
+               block.link = hex.toBytes(BURN_PUBLIC_KEY)
+
+               return block
+       } catch (err) {
+               block.link = link
+               block.representative = rep
+               block.subtype = subtype
+               throw new TypeError('Failed to configure change block', { cause: err })
+       }
+}
index 9311d5f000bc1aa123e518c39d3eeb953c7ab75a..7e0adad475fbc2759980e26a482017728765030e 100644 (file)
@@ -2,14 +2,19 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { NanoPow } from 'nano-pow'
-import { derive as nano25519_derive, sign as nano25519_sign, verify as nano25519_verify } from 'nano25519/sync'
 import { Account } from '../account'
-import { BURN_PUBLIC_KEY, DIFFICULTY_RECEIVE, DIFFICULTY_SEND, PREAMBLE, UNITS } from '../constants'
+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 { Wallet } from '../wallet'
+import { _change } from './change'
+import { _receive } from './receive'
+import { _send } from './send'
+import { _sign } from './sign'
+import { _validate } from './validate'
+import { _verify } from './verify'
 
 /**
  * Represents a block as defined by the Nano cryptocurrency protocol.
@@ -23,50 +28,7 @@ export class Block {
         * @param {Block} block - SendBlock, ReceiveBlock, or ChangeBlock to validate
         */
        static validate (block: unknown): asserts block is Block {
-               if (typeof block !== 'object') {
-                       throw new TypeError('Invalid block')
-               }
-               const b = block as Record<string, unknown>
-               if (b.account == null) {
-                       throw new Error('Account missing')
-               }
-               if (b.previous == null || b.previous === '') {
-                       throw new Error('Frontier missing')
-               }
-               if (b.representative == null) {
-                       throw new Error('Representative missing')
-               }
-               if (b.balance == null) {
-                       throw new Error('Balance missing')
-               }
-               if (typeof b.balance !== 'number' && typeof b.balance !== 'bigint') {
-                       throw new TypeError('Balance must be number or bigint')
-               }
-               if (b.balance < 0) {
-                       throw new Error('Negative balance')
-               }
-               if (b.subtype === 'send') {
-                       if (b.link == null || b.link === '') {
-                               throw new Error('Recipient missing')
-                       }
-               }
-               if (b.subtype === 'receive') {
-                       if (b.link == null) {
-                               throw new Error('Origin send block hash missing')
-                       }
-               }
-               if (b.subtype === 'change') {
-                       const { link } = b
-                       if (link == null) {
-                               throw new Error('Change block link missing')
-                       }
-                       if (!(link instanceof Uint8Array)) {
-                               throw new Error('Invalid change block link')
-                       }
-                       if (link.some(b => b !== 0)) {
-                               throw new Error('Change block link must be zero')
-                       }
-               }
+               _validate(block)
        }
 
        subtype?: 'send' | 'receive' | 'change'
@@ -189,31 +151,8 @@ export class Block {
         * @param {(string|Account)} account - Account to choose as representative, or its address or public key
         * @returns {Block} This block with link, representative, and subtype configured
         */
-       change (representative: string | Account): Block
-       change (representative: unknown): Block {
-               const { link, representative: rep, subtype } = this
-               try {
-                       if (this.subtype != null) {
-                               throw new Error(`Block already configured as ${this.subtype}`)
-                       }
-                       this.subtype = 'change'
-
-                       if (typeof representative !== 'string' && !(representative instanceof Account)) {
-                               throw new TypeError('Invalid account')
-                       }
-                       this.representative = (typeof representative === 'string')
-                               ? new Account(representative)
-                               : representative
-
-                       this.link = hex.toBytes(BURN_PUBLIC_KEY)
-
-                       return this
-               } catch (err) {
-                       this.link = link
-                       this.representative = rep
-                       this.subtype = subtype
-                       throw new TypeError('Failed to configure change block', { cause: err })
-               }
+       change (representative: string | Account): Block {
+               return _change(this, representative)
        }
 
        /**
@@ -286,39 +225,8 @@ export class Block {
         * @param {string} [unit] - Unit of measure for amount (e.g. 'NANO' = 10³⁰ RAW). Default: "RAW"
         * @returns {Block} This block with balance, link, and subtype configured
        */
-       receive (sendBlock: string | Block, amount: bigint | number | string, unit?: string): Block
-       receive (sendBlock: unknown, amount: unknown, unit: unknown): Block {
-               const { balance, link, subtype } = this
-               try {
-                       if (this.subtype != null) {
-                               throw new Error(`Block already configured as ${this.subtype}`)
-                       }
-                       this.subtype = 'receive'
-
-                       unit ??= 'RAW'
-                       if (typeof unit !== 'string' || typeof UNITS[unit] !== 'number') {
-                               throw new TypeError('Invalid unit')
-                       }
-
-                       if (typeof amount !== 'bigint' && typeof amount !== 'number' && typeof amount !== 'string') {
-                               throw new TypeError('Invalid amount')
-                       }
-                       this.balance += Tools.convert(amount, unit, 'raw', 'bigint')
-
-                       if (typeof sendBlock !== 'string' && !(sendBlock instanceof (this.constructor as typeof Block))) {
-                               throw new TypeError('Invalid send block')
-                       }
-                       this.link = (typeof sendBlock === 'string')
-                               ? hex.toBytes(sendBlock)
-                               : hex.toBytes(sendBlock.hash)
-
-                       return this
-               } catch (err) {
-                       this.balance = balance
-                       this.link = link
-                       this.subtype = subtype
-                       throw new TypeError('Failed to configure receive block', { cause: err })
-               }
+       receive (sendBlock: string | Block, amount: bigint | number | string, unit?: string): Block {
+               return _receive(this, sendBlock, amount, unit)
        }
 
        /**
@@ -329,43 +237,8 @@ export class Block {
         * @param {string} [unit] - Unit of measure for amount (e.g. 'NANO' = 10³⁰ RAW). Default: "RAW"
         * @returns {Block} This block with balance, link, and subtype configured
         */
-       send (account: string | Account, amount: bigint | number | string, unit?: string): Block
-       send (account: unknown, amount: unknown, unit: unknown): Block {
-               const { balance, link, subtype } = this
-               try {
-                       if (this.subtype != null) {
-                               throw new Error(`Block already configured as ${this.subtype}`)
-                       }
-                       this.subtype = 'send'
-
-                       unit ??= 'RAW'
-                       if (typeof unit !== 'string' || typeof UNITS[unit] !== 'number') {
-                               throw new TypeError('Invalid unit', { cause: unit })
-                       }
-
-                       if (typeof amount !== 'bigint' && typeof amount !== 'number' && typeof amount !== 'string') {
-                               throw new TypeError(`Invalid amount ${amount}`, { cause: typeof amount })
-                       }
-                       this.balance -= Tools.convert(amount, unit, 'raw', 'bigint')
-
-                       if (this.balance < 0) {
-                               throw new RangeError('Insufficient funds', { cause: this.balance })
-                       }
-
-                       if (typeof account !== 'string' && !(account instanceof Account)) {
-                               throw new TypeError('Invalid account', { cause: account })
-                       }
-                       this.link = (typeof account === 'string')
-                               ? hex.toBytes(new Account(account).publicKey)
-                               : hex.toBytes(account.publicKey)
-
-                       return this
-               } catch (err) {
-                       this.balance = balance
-                       this.link = link
-                       this.subtype = subtype
-                       throw new TypeError('Failed to configure send block', { cause: err })
-               }
+       send (account: string | Account, amount: bigint | number | string, unit?: string): Block {
+               return _send(this, account, amount, unit)
        }
 
        /**
@@ -399,35 +272,8 @@ export class Block {
         * @returns Block with `signature` value set
         */
        async sign (wallet: Wallet, index: number, frontier?: Block): Promise<Block>
-       sign (input: unknown, index?: unknown, frontier?: unknown): Block | Promise<Block> {
-               if (navigator.userActivation?.isActive === false) {
-                       throw new DOMException(
-                               'Signing request was blocked due to lack of user activation',
-                               'NotAllowedError'
-                       )
-               }
-               if (this.signature !== undefined) {
-                       throw new TypeError('Block signature already exists', { cause: this.signature })
-               }
-               try {
-                       if (typeof input === 'string' && /^[A-F0-9]{64}$/i.test(input)) {
-                               const pub = nano25519_derive(input)
-                               input += pub
-                       }
-                       if (typeof input === 'string' && /^[A-F0-9]{128}$/i.test(input)) {
-                               this.signature = nano25519_sign(this.hash, input)
-                               return this
-                       } else if (input instanceof Wallet && typeof index === 'number'
-                               && (frontier === undefined || frontier instanceof (this.constructor as typeof Block))
-                       ) {
-                               return input.sign(index, this, frontier)
-                       } else {
-                               throw new TypeError('Invalid input for block signature')
-                       }
-               } catch (err) {
-                       console.error(err)
-                       throw new Error('Failed to sign block', { cause: err })
-               }
+       sign (wallet: string | Wallet, index?: number, frontier?: Block): Block | Promise<Block> {
+               return _sign(this, wallet, index, frontier)
        }
 
        /**
@@ -445,17 +291,7 @@ export class Block {
         * @param {string} [publicKey] - 32-byte hexadecimal public key to use for verification
         * @returns {boolean} True if block was signed by the matching private key
         */
-       verify (publicKey?: string): boolean
-       verify (input: unknown): boolean {
-               input ??= this.account.publicKey
-               if (typeof input !== 'string') {
-                       throw new Error('Invalid input')
-               }
-               try {
-                       const account = new Account(input)
-                       return nano25519_verify(this.signature ?? '', this.hash, account.publicKey)
-               } catch (err) {
-                       throw new Error('Failed to verify block signature', { cause: err })
-               }
+       verify (publicKey?: string): boolean {
+               return _verify(this, publicKey)
        }
 }
diff --git a/src/lib/block/receive.ts b/src/lib/block/receive.ts
new file mode 100644 (file)
index 0000000..7d82373
--- /dev/null
@@ -0,0 +1,50 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import type { Block } from '.'
+import { UNITS } from '../constants'
+import { hex } from '../convert'
+import { Tools } from '../tools'
+
+/**
+ * Set the amount of nano that this block will receive from a corresponding
+ * send block.
+ *
+ * @param {(string|Block)} sendBlock - Corresponding send block or its hash
+ * @param {(bigint|number|string)} amount - Amount to be received from sender
+ * @param {string} [unit] - Unit of measure for amount (e.g. 'NANO' = 10³⁰ RAW). Default: "RAW"
+ * @returns {Block} This block with balance, link, and subtype configured
+*/
+export function _receive (block: Block, sendBlock: unknown, amount: unknown, unit: unknown): Block {
+       const { balance, link, subtype } = block
+       try {
+               if (block.subtype != null) {
+                       throw new Error(`Block already configured as ${block.subtype}`)
+               }
+               block.subtype = 'receive'
+
+               unit ??= 'RAW'
+               if (typeof unit !== 'string' || typeof UNITS[unit] !== 'number') {
+                       throw new TypeError('Invalid unit')
+               }
+
+               if (typeof amount !== 'bigint' && typeof amount !== 'number' && typeof amount !== 'string') {
+                       throw new TypeError('Invalid amount')
+               }
+               block.balance += Tools.convert(amount, unit, 'raw', 'bigint')
+
+               if (typeof sendBlock !== 'string' && !(sendBlock instanceof (block.constructor as typeof Block))) {
+                       throw new TypeError('Invalid send block')
+               }
+               block.link = (typeof sendBlock === 'string')
+                       ? hex.toBytes(sendBlock)
+                       : hex.toBytes(sendBlock.hash)
+
+               return block
+       } catch (err) {
+               block.balance = balance
+               block.link = link
+               block.subtype = subtype
+               throw new TypeError('Failed to configure receive block', { cause: err })
+       }
+}
diff --git a/src/lib/block/send.ts b/src/lib/block/send.ts
new file mode 100644 (file)
index 0000000..e49df01
--- /dev/null
@@ -0,0 +1,53 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import type { Block } from '.'
+import { Account } from '../account'
+import { UNITS } from '../constants'
+import { hex } from '../convert'
+import { Tools } from '../tools'
+/**
+ * Set the amount of nano that this block will send to a recipient account.
+ *
+ * @param {(string|Account)} account - Account to target or its address or public key
+ * @param {(bigint|number|string)} amount - Amount to send to recipient
+ * @param {string} [unit] - Unit of measure for amount (e.g. 'NANO' = 10³⁰ RAW). Default: "RAW"
+ * @returns {Block} This block with balance, link, and subtype configured
+ */
+export function _send (block: Block, account: unknown, amount: unknown, unit: unknown): Block {
+       const { balance, link, subtype } = block
+       try {
+               if (block.subtype != null) {
+                       throw new Error(`Block already configured as ${block.subtype}`)
+               }
+               block.subtype = 'send'
+
+               unit ??= 'RAW'
+               if (typeof unit !== 'string' || typeof UNITS[unit] !== 'number') {
+                       throw new TypeError('Invalid unit', { cause: unit })
+               }
+
+               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')
+
+               if (block.balance < 0) {
+                       throw new RangeError('Insufficient funds', { cause: block.balance })
+               }
+
+               if (typeof account !== 'string' && !(account instanceof Account)) {
+                       throw new TypeError('Invalid account', { cause: account })
+               }
+               block.link = (typeof account === 'string')
+                       ? hex.toBytes(new Account(account).publicKey)
+                       : hex.toBytes(account.publicKey)
+
+               return block
+       } catch (err) {
+               block.balance = balance
+               block.link = link
+               block.subtype = subtype
+               throw new TypeError('Failed to configure send block', { cause: err })
+       }
+}
diff --git a/src/lib/block/sign.ts b/src/lib/block/sign.ts
new file mode 100644 (file)
index 0000000..cbb115b
--- /dev/null
@@ -0,0 +1,37 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { derive as nano25519_derive, sign as nano25519_sign } from 'nano25519'
+import type { Block } from '.'
+import { Wallet } from '../wallet'
+
+export function _sign (block: Block, input: unknown, index: unknown, frontier: unknown): Block | Promise<Block> {
+       if (navigator.userActivation?.isActive === false) {
+               throw new DOMException(
+                       'Signing request was blocked due to lack of user activation',
+                       'NotAllowedError'
+               )
+       }
+       if (block.signature !== undefined) {
+               throw new TypeError('Block signature already exists', { cause: block.signature })
+       }
+       try {
+               if (typeof input === 'string' && /^[A-F0-9]{64}$/i.test(input)) {
+                       const pub = nano25519_derive(input)
+                       input += pub
+               }
+               if (typeof input === 'string' && /^[A-F0-9]{128}$/i.test(input)) {
+                       block.signature = nano25519_sign(block.hash, input)
+                       return block
+               } else if (input instanceof Wallet && typeof index === 'number'
+                       && (frontier === undefined || frontier instanceof (block.constructor as typeof Block))
+               ) {
+                       return input.sign(index, block, frontier)
+               } else {
+                       throw new TypeError('Invalid input for block signature')
+               }
+       } catch (err) {
+               console.error(err)
+               throw new Error('Failed to sign block', { cause: err })
+       }
+}
diff --git a/src/lib/block/validate.ts b/src/lib/block/validate.ts
new file mode 100644 (file)
index 0000000..979115e
--- /dev/null
@@ -0,0 +1,51 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import type { Block } from '.'
+
+export function _validate (block: unknown): asserts block is Block {
+       if (typeof block !== 'object') {
+               throw new TypeError('Invalid block')
+       }
+       const b = block as Record<string, unknown>
+       if (b.account == null) {
+               throw new Error('Account missing')
+       }
+       if (b.previous == null || b.previous === '') {
+               throw new Error('Frontier missing')
+       }
+       if (b.representative == null) {
+               throw new Error('Representative missing')
+       }
+       if (b.balance == null) {
+               throw new Error('Balance missing')
+       }
+       if (typeof b.balance !== 'number' && typeof b.balance !== 'bigint') {
+               throw new TypeError('Balance must be number or bigint')
+       }
+       if (b.balance < 0) {
+               throw new Error('Negative balance')
+       }
+       if (b.subtype === 'send') {
+               if (b.link == null || b.link === '') {
+                       throw new Error('Recipient missing')
+               }
+       }
+       if (b.subtype === 'receive') {
+               if (b.link == null) {
+                       throw new Error('Origin send block hash missing')
+               }
+       }
+       if (b.subtype === 'change') {
+               const { link } = b
+               if (link == null) {
+                       throw new Error('Change block link missing')
+               }
+               if (!(link instanceof Uint8Array)) {
+                       throw new Error('Invalid change block link')
+               }
+               if (link.some(b => b !== 0)) {
+                       throw new Error('Change block link must be zero')
+               }
+       }
+}
diff --git a/src/lib/block/verify.ts b/src/lib/block/verify.ts
new file mode 100644 (file)
index 0000000..53595d0
--- /dev/null
@@ -0,0 +1,19 @@
+//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { verify as nano25519_verify } from 'nano25519'
+import type { Block } from '.'
+import { Account } from '../account'
+
+export function _verify (block: Block, input: unknown): boolean {
+       input ??= block.account.publicKey
+       if (typeof input !== 'string') {
+               throw new Error('Invalid input')
+       }
+       try {
+               const account = new Account(input)
+               return nano25519_verify(block.signature ?? '', block.hash, account.publicKey)
+       } catch (err) {
+               throw new Error('Failed to verify block signature', { cause: err })
+       }
+}