From c114615a0da0327118d94e57b4416581bed0f434 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sat, 27 Jun 2026 23:21:05 -0700 Subject: [PATCH] Extract common block functions to separate files. --- src/lib/block/change.ts | 33 +++++++ src/lib/block/index.ts | 200 ++++---------------------------------- src/lib/block/receive.ts | 50 ++++++++++ src/lib/block/send.ts | 53 ++++++++++ src/lib/block/sign.ts | 37 +++++++ src/lib/block/validate.ts | 51 ++++++++++ src/lib/block/verify.ts | 19 ++++ 7 files changed, 261 insertions(+), 182 deletions(-) create mode 100644 src/lib/block/change.ts create mode 100644 src/lib/block/receive.ts create mode 100644 src/lib/block/send.ts create mode 100644 src/lib/block/sign.ts create mode 100644 src/lib/block/validate.ts create mode 100644 src/lib/block/verify.ts diff --git a/src/lib/block/change.ts b/src/lib/block/change.ts new file mode 100644 index 0000000..a8d7748 --- /dev/null +++ b/src/lib/block/change.ts @@ -0,0 +1,33 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! 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 }) + } +} diff --git a/src/lib/block/index.ts b/src/lib/block/index.ts index 9311d5f..7e0adad 100644 --- a/src/lib/block/index.ts +++ b/src/lib/block/index.ts @@ -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 - 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 - sign (input: unknown, index?: unknown, frontier?: unknown): Block | Promise { - 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 { + 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 index 0000000..7d82373 --- /dev/null +++ b/src/lib/block/receive.ts @@ -0,0 +1,50 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! 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 index 0000000..e49df01 --- /dev/null +++ b/src/lib/block/send.ts @@ -0,0 +1,53 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! 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 index 0000000..cbb115b --- /dev/null +++ b/src/lib/block/sign.ts @@ -0,0 +1,37 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! 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 { + 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 index 0000000..979115e --- /dev/null +++ b/src/lib/block/validate.ts @@ -0,0 +1,51 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! 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 + 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 index 0000000..53595d0 --- /dev/null +++ b/src/lib/block/verify.ts @@ -0,0 +1,19 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! 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 }) + } +} -- 2.52.0