]> git.codecow.com Git - libnemo.git/commitdiff
Refactor Block to be a concrete class instead of abstract.
authorChris Duncan <chris@zoso.dev>
Wed, 6 Aug 2025 04:37:39 +0000 (21:37 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 6 Aug 2025 04:37:39 +0000 (21:37 -0700)
src/lib/block.ts

index 7b5571e437522da0d3e8326a3da9faa4e773ea1a..0bf814c030e4ff721c25b89a61b19cef68c89362 100644 (file)
@@ -15,28 +15,34 @@ import { Wallet } from './wallet'
 * class is abstract and cannot be directly instantiated. Every block must be one
 * of three derived classes: SendBlock, ReceiveBlock, ChangeBlock.
 */
-abstract class Block {
+class Block {
+       subtype?: 'send' | 'receive' | 'change'
        account: Account
-       type: 'state' = 'state'
-       abstract subtype?: 'send' | 'receive' | 'change'
-       abstract previous: string
-       abstract representative?: Account
-       abstract balance: bigint
-       abstract link: string
-       abstract signature?: string
-       abstract work?: string
+       balance: bigint
+       previous: string
+       representative: Account
+       link?: string
+       signature?: string
+       work?: string
 
-       constructor (account: Account | string) {
-               if (this.constructor === Block) {
-                       throw new Error('Block is an abstract class and cannot be instantiated directly.')
-               }
-               if (account instanceof Account) {
-                       this.account = account
-               } else if (typeof account === 'string') {
-                       this.account = Account.import(account)
-               } else {
+       constructor (account: Account)
+       constructor (account: unknown) {
+               if (!(account instanceof Account)) {
                        throw new TypeError('Invalid account')
                }
+               if (account.balance == null) {
+                       throw new TypeError('Account balance is unknown')
+               }
+               if (account.frontier == null) {
+                       throw new TypeError('Account frontier is unknown')
+               }
+               if (account.representative == null) {
+                       throw new TypeError('Account representative is unknown')
+               }
+               this.account = account
+               this.balance = account.balance
+               this.previous = account.frontier
+               this.representative = account.representative
        }
 
        /**
@@ -45,20 +51,25 @@ abstract class Block {
        * @returns {Promise<string>} Block data hashed to a byte array
        */
        get hash (): string {
-               if (this.representative == null) {
-                       throw new Error('Block hash missing representative')
+               try {
+                       if (this.link == null) {
+                               throw new Error('Block link is required')
+                       }
+                       const data = [
+                               PREAMBLE,
+                               this.account.publicKey,
+                               this.previous.padStart(64, '0'),
+                               this.representative.publicKey,
+                               dec.toHex(this.balance, 32),
+                               this.link.padStart(64, '0')
+                       ]
+                       const hash = new Blake2b(32)
+                       data.forEach(str => hash.update(hex.toBytes(str)))
+                       return hash.digest('hex').toUpperCase()
+               } catch (err) {
+                       console.error(err)
+                       throw new Error('Failed to hash block', { cause: err })
                }
-               const data = [
-                       PREAMBLE,
-                       this.account.publicKey,
-                       this.previous.padStart(64, '0'),
-                       this.representative.publicKey,
-                       dec.toHex(this.balance, 32),
-                       this.link.padStart(64, '0')
-               ]
-               const hash = new Blake2b(32)
-               data.forEach(str => hash.update(hex.toBytes(str)))
-               return hash.digest('hex').toUpperCase()
        }
 
        /**
@@ -67,15 +78,23 @@ abstract class Block {
        * @returns {string} JSON representation of the block
        */
        toJSON (): { [key: string]: string } {
-               return {
-                       "type": this.type,
-                       "account": this.account.address,
-                       "previous": this.previous,
-                       "representative": this.representative?.address ?? '',
-                       "balance": this.balance.toString(),
-                       "link": this.link,
-                       "signature": this.signature ?? '',
-                       "work": this.work ?? ''
+               try {
+                       if (this.link == null) {
+                               throw new Error('Block link is required')
+                       }
+                       return {
+                               "type": "state",
+                               "account": this.account.address,
+                               "previous": this.previous,
+                               "representative": this.representative.address ?? '',
+                               "balance": this.balance.toString(),
+                               "link": this.link,
+                               "signature": this.signature ?? '',
+                               "work": this.work ?? ''
+                       }
+               } catch (err) {
+                       console.error(err)
+                       throw new Error('Failed to stringify block', { cause: err })
                }
        }
 
@@ -90,9 +109,11 @@ abstract class Block {
                }
                try {
                        this.subtype = 'change'
+                       this.link = Account.import(BURN_ADDRESS).publicKey
                        return this
                } catch (err) {
                        this.subtype = undefined
+                       this.link = undefined
                        throw new TypeError('Failed to configure send block', { cause: err })
                }
        }
@@ -122,7 +143,6 @@ abstract class Block {
                        this.link = (typeof send === 'string')
                                ? send
                                : send.hash
-                       this.representative = this.account.representative
                        return this
                } catch (err) {
                        throw new Error('Failed to receive a corresponding send', { cause: err })
@@ -135,7 +155,7 @@ abstract class Block {
        * A successful response sets the `work` property.
        */
        async pow (): Promise<Block> {
-               const difficulty: bigint = (this instanceof SendBlock || this instanceof ChangeBlock)
+               const difficulty: bigint = (this.subtype === 'send' || this.subtype === 'change')
                        ? DIFFICULTY_SEND
                        : DIFFICULTY_RECEIVE
                const result = await NanoPow.work_generate(this.previous, { difficulty })
@@ -163,6 +183,7 @@ abstract class Block {
        * @returns {Promise<string>} Hash of the processed block
        */
        async process (rpc: Rpc): Promise<string> {
+               validate(this)
                if (!this.signature) {
                        throw new Error('Block is missing signature. Use sign() and try again.')
                }
@@ -325,7 +346,8 @@ abstract class Block {
                                return this
 
                        } else if (input instanceof Wallet && typeof param === 'number') {
-                               const sig = await input.sign(param, this as SendBlock | ReceiveBlock | ChangeBlock)
+                               const wallet = input
+                               const sig = await wallet.sign(param, this)
                                if (this.signature !== sig) {
                                        throw new Error('Wallet signature does not match block signature')
                                }
@@ -336,14 +358,14 @@ abstract class Block {
                                const { Ledger } = await import('./ledger')
                                const ledger = await Ledger.create()
                                await ledger.connect()
-                               if (param && (param instanceof ChangeBlock || param instanceof ReceiveBlock || param instanceof SendBlock)) {
+                               if (param && param instanceof Block) {
                                        try {
                                                await ledger.updateCache(index, param)
                                        } catch (err) {
                                                console.warn('Error updating Ledger cache of previous block, attempting signature anyway', err)
                                        }
                                }
-                               this.signature = await ledger.sign(index, this as SendBlock | ReceiveBlock | ChangeBlock)
+                               this.signature = await ledger.sign(index, this)
                                return this
 
                        } else {
@@ -379,7 +401,6 @@ abstract class Block {
                        }
                        switch (this.subtype) {
                                case 'change': {
-                                       this.link = Account.import(BURN_ADDRESS).publicKey
                                        this.representative = (typeof account === 'string')
                                                ? Account.import(account)
                                                : account
@@ -389,7 +410,6 @@ abstract class Block {
                                        this.link = (typeof account === 'string')
                                                ? Account.import(account).publicKey
                                                : account.publicKey
-                                       this.representative = this.account.representative
                                        break
                                }
                                default: {
@@ -422,118 +442,6 @@ abstract class Block {
        }
 }
 
-/**
-* Represents a block that sends funds from one address to another as defined by
-* the Nano cryptocurrency protocol.
-*/
-export class SendBlock extends Block {
-       type: 'state' = 'state'
-       subtype: 'send' = 'send'
-       previous: string
-       representative: Account
-       balance: bigint
-       link: string
-       signature?: string
-       work?: string
-
-       constructor (
-               sender: Account | string,
-               balance: string,
-               recipient: string,
-               amount: string,
-               representative: Account | string,
-               frontier: string,
-               work?: string
-       ) {
-               if (typeof sender === 'string') sender = Account.import(sender)
-               if (typeof representative === 'string') representative = Account.import(representative)
-               super(sender)
-               this.previous = frontier
-               this.representative = representative
-               this.link = Account.import(recipient).publicKey
-               this.work = work ?? ''
-
-               const bigBalance = BigInt(balance)
-               const bigAmount = BigInt(amount)
-               this.balance = bigBalance - bigAmount
-
-               validate(this)
-       }
-}
-
-/**
-* Represents a block that receives funds sent to one address from another as
-* defined by the Nano cryptocurrency protocol.
-*/
-export class ReceiveBlock extends Block {
-       type: 'state' = 'state'
-       subtype: 'receive' = 'receive'
-       previous: string
-       representative: Account
-       balance: bigint
-       link: string
-       signature?: string
-       work?: string
-
-       constructor (
-               recipient: Account | string,
-               balance: string,
-               origin: string,
-               amount: string,
-               representative: Account | string,
-               frontier?: string,
-               work?: string
-       ) {
-               if (typeof recipient === 'string') recipient = Account.import(recipient)
-               if (typeof representative === 'string') representative = Account.import(representative)
-               super(recipient)
-               this.previous = frontier ?? recipient.publicKey
-               this.representative = representative
-               this.link = origin
-               this.work = work ?? ''
-
-               const bigBalance = BigInt(balance)
-               const bigAmount = BigInt(amount)
-               this.balance = bigBalance + bigAmount
-
-               validate(this)
-       }
-}
-
-/**
-* Represents a block that changes the representative account to which the user
-* account delegates their vote weight using the the Open Representative Voting
-* specification as defined by the Nano cryptocurrency protocol.
-*/
-export class ChangeBlock extends Block {
-       type: 'state' = 'state'
-       subtype: 'change' = 'change'
-       previous: string
-       representative: Account
-       balance: bigint
-       link: string = Account.import(BURN_ADDRESS).publicKey
-       signature?: string
-       work?: string
-
-       constructor (
-               account: Account | string,
-               balance: string,
-               representative: Account | string,
-               frontier: string,
-               work?: string
-       ) {
-               if (typeof account === 'string') account = Account.import(account)
-               if (typeof representative === 'string') representative = Account.import(representative)
-               super(account)
-               this.previous = frontier
-               this.representative = representative
-               this.balance = BigInt(balance)
-               this.work = work ?? ''
-
-               validate(this)
-       }
-}
-
 /**
  * Validates block data.
  *
@@ -562,17 +470,17 @@ function validate (block: unknown): asserts block is Block {
        if (b.balance < 0) {
                throw new Error('Negative balance')
        }
-       if (b instanceof SendBlock) {
+       if (b.subtype === 'send') {
                if (b.link == null || b.link === '') {
                        throw new Error('Recipient missing')
                }
        }
-       if (b instanceof ReceiveBlock) {
+       if (b.subtype === 'receive') {
                if (b.link == null) {
                        throw new Error('Origin send block hash missing')
                }
        }
-       if (b instanceof ChangeBlock) {
+       if (b.subtype === 'change') {
                if (b.link == null) {
                        throw new Error('Change block link missing')
                }