//! 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.
* @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'
* @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)
}
/**
* @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)
}
/**
* @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)
}
/**
* @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)
}
/**
* @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)
}
}
--- /dev/null
+//! 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 })
+ }
+}
--- /dev/null
+//! 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 })
+ }
+}